Using CakePHP 3.5.3
Hi,
// WHAT I HAVE
I have a users index view which displays all the relevant users in the database. At the top of the page I have a select list which allows the user to select the deactivate option and on each users row I have a checkbox.
// WHAT I'M TRYING TO DO
The user can check the checkboxes they want then select the deactivate option from the select list and click submit. The submit sends the script
to my actions method in my users controller. In the actions method I check to see if the deactivate option has been selected and if it hasn't I send a
message back to the user to that effect. After this I check to see if a checkbox has been checked and if none have I send a message back to the user to that effect.
If at least one checkbox has been checked and the deactivate option has been selected I then identify which checkboxes have been checked and
update the database to deactivate for those choosen users.
// ON USERS INDEX VIEW I DECLARE MY CHECKBOX AS BELOW
<?= $this->Form->checkbox("checkboxes[]", ['value' => $user->id]); ?>
// ACTIONS METHOD
public function actions()
{
if (empty($this->request->getData('action'))) {
$this->Flash->sys_error('', [
'key' => 'message',
'params' => [
'p1' => __('Please select an action!')
]
]);
// Send to index action for page load
$this->setAction('index');
}
else {
$checkboxArray = $this->request->getData('checkboxes');
foreach($checkboxArray as $key => $val):
echo 'key is ' . $key . "<br />";
echo 'val is ' . $val . "<br />";
if (empty($val)) {
unset($checkboxArray[$key]);
}
endforeach;
if (empty($checkboxArray)) {
$this->Flash->sys_error('', [
'key' => 'message',
'params' => [
'p1' => __('Please select a user!')
]
]);
// Send to index action for page load
$this->setAction('index');
}
else {
foreach($this->request->getData('checkboxes') as $userID):
if ($userID != 0) {
// UPDATE THE DATABSE
echo 'in update the database ' . '<hr />';
}
endforeach;
// Send to index action for page load
$this->setAction('index');
}
}
}
Even though I'm not 100% sure if this the best or correct way to do this it does work except for the following error which occurs on the checkbox in the users index view each time I invoke $this->setAction('index'); The stacktrace from the log is as follows:
Notice (8): Array to string conversion in [C:\xampp\htdocs\app\vendor
\cakephp\cakephp\src\View\Widget\CheckboxWidget.php, line 81] Request
URL: /users/actions Referer URL: https://localhost/app/users Trace:
Cake\Error\BaseErrorHandler::handleError() -
CORE\src\Error\BaseErrorHandler.php, line 153
Cake\View\Widget\CheckboxWidget::_isChecked() -
CORE\src\View\Widget\CheckboxWidget.php, line 81
Cake\View\Widget\CheckboxWidget::render() -
CORE\src\View\Widget\CheckboxWidget.php, line 51
Cake\View\Helper\FormHelper::widget() -
CORE\src\View\Helper\FormHelper.php, line 2775
Cake\View\Helper\FormHelper::checkbox() -
CORE\src\View\Helper\FormHelper.php, line 1570 include -
APP/Template\Users\index.ctp, line 193 Cake\View\View::_evaluate() -
CORE\src\View\View.php, line 1196 Cake\View\View::_render() -
CORE\src\View\View.php, line 1157 Cake\View\View::render() -
CORE\src\View\View.php, line 765 Cake\Controller\Controller::render()
- CORE\src\Controller\Controller.php, line 623 Cake\Http\ActionDispatcher::_invoke() -
CORE\src\Http\ActionDispatcher.php, line 125
Cake\Http\ActionDispatcher::dispatch() -
CORE\src\Http\ActionDispatcher.php, line 93
Cake\Http\BaseApplication::__invoke() -
CORE\src\Http\BaseApplication.php, line 103
Cake\Http\Runner::__invoke() - CORE\src\Http\Runner.php, line 65
Cake\Http\Middleware\CsrfProtectionMiddleware::__invoke() -
CORE\src\Http\Middleware\CsrfProtectionMiddleware.php, line 106
Cake\Http\Runner::__invoke() - CORE\src\Http\Runner.php, line 65
Cake\I18n\Middleware\LocaleSelectorMiddleware::__invoke() -
CORE\src\I18n\Middleware\LocaleSelectorMiddleware.php, line 62
Cake\Http\Runner::__invoke() - CORE\src\Http\Runner.php, line 65
Cake\Routing\Middleware\RoutingMiddleware::__invoke() -
CORE\src\Routing\Middleware\RoutingMiddleware.php, line 107
Cake\Http\Runner::__invoke() - CORE\src\Http\Runner.php, line 65
Cake\Routing\Middleware\AssetMiddleware::__invoke() -
CORE\src\Routing\Middleware\AssetMiddleware.php, line 88
Cake\Http\Runner::__invoke() - CORE\src\Http\Runner.php, line 65
Cake\Error\Middleware\ErrorHandlerMiddleware::__invoke() -
CORE\src\Error\Middleware\ErrorHandlerMiddleware.php, line 95
Cake\Http\Runner::__invoke() - CORE\src\Http\Runner.php, line 65
DebugKit\Middleware\DebugKitMiddleware::__invoke() -
ROOT\vendor\cakephp\debug_kit\src\Middleware\DebugKitMiddleware.php,
line 52 Cake\Http\Runner::__invoke() - CORE\src\Http\Runner.php, line
65 Cake\Http\Runner::run() - CORE\src\Http\Runner.php, line 51
Cake\Http\Server::run() - CORE\src\Http\Server.php, line 81 [main] -
ROOT\webroot\index.php, line 40
// SOLUTION
I have managed to eliminate this error if I declare my checkbox as below:
// FROM
<?= $this->Form->checkbox("checkboxes[]", ['value' => $user->id]); ?>
// TO
<?= $this->Form->checkbox("checkboxes[$user->id]", ['value' => user->id]); ?>
// MY QUESTION
But this means I'm replacing the keys in the array with the user id and I don't know if this is considered as OK or if there's a better more professional way of doing it.
Any help would be greatly appreciated. Z.
#SamHecquet - Thanks for the edit, I'll know for next time how to format the stacktrace.
#Derek - As requested see below - I won't include the whole page as a lot of it I believe is unnecessary but if you want more than I have posted just let me know.
// Select list from the element
<!-- Start form -->
<?php
// Declare individual page forms
if ($page === 'usersPage') {
// Users form
echo $this->Form->create(null, [
'url' => ['controller' => 'Users', 'action' => 'actions'],
'novalidate' => true
]);
}
<?php
// Declare individual page select lists
if ($page === 'usersPage') {
// Users form
$options = [
'' => '--Select Action--',
'a' => 'Deactivate User/s'
];
}
else{
header ("Location: ".$this->Url->build(["controller" => "Pages", "action" => "display", 'blank']));
exit(0);
}
$this->Form->label('action', '');
echo $this->Form->input('action', [
'type' => 'select',
'options' => $options,
'class' => 'ns-typ-2 ix-fa',
'label' => false
]);
?>
<?= $this->Form->submit(__('Go'), [
'class' => 'submit-ix-go'
]);
?>
// INDEX USERS
<!-- Headings -->
<div class="grid-ix-1 ix-r-top">
<!-- Checkbox -->
<div class="ix-usr-checkbox">
<div class="ix-h">
<div class="ix-h-cb">
<input type="checkbox" onClick="toggle(this)" >
</div>
</div>
</div>
<!-- Role -->
<div class="ix-usr-role">
<div class="ix-h">
<h2 class="ix-h2"><?= $this->Paginator->sort('Users.role', __('Role')) ?></h2>
</div>
</div>
</div>
<!-- Cells-->
foreach ($users as $user): ?>
<div class="grid-ix-1">
<!-- Checkbox -->
<div class="ix-usr-checkbox">
<div class="ix-c-cb">
<?= $this->Form->checkbox('checkboxes[]', ['value' => $user->id]); ?>
</div>
</div>
<!-- Role -->
<div class="ix-usr-role">
<div class="ix-usr-role-text"><?= h($user->role) ?></div>
</div>
</div>
<?php endforeach; ?>
<!-- Form ends -->
<?= $this->Form->end() ?>
#Derek - Please see the multiple checkbox issue.
When I used the solution from Dave the multiple checkbox selection worked on page load but not after I invoked the below:
// Send to index action for page load
$this->setAction('index');
I fixed this by redirecting instead of using setAction as below:
// Redirect to index page
return $this->redirect(['action' => 'index']);
But with your solution the multiple checkbox selection functionality did not work on page load or after any of the redirects. I use the following code to achieve this.
// Javascript
// Check and uncheck all check boxes on index pages
function toggle(source) {
checkboxes = document.getElementsByName('checkboxes[]');
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = source.checked;
}
}
// And then the heading checkbox is:
<input type="checkbox" onClick="toggle(this)" >
// And of course the cells checkbox
<?= $this->Form->checkbox('checkboxes.' . $k, ['value' => $user->id]); ?>
If you help with the javascript great but if not if you advise that a solution can be found I'm happy to post in the stackoverflow javascript section for a solution which would mean I could go with your solution and not edit the framework code.
Thanks in advance for any help or advice. Z
Try changing the foreach to:
<?php foreach ($users as $k => $user): ?>
<div class="grid-ix-1">
<!-- Checkbox -->
<div class="ix-usr-checkbox">
<div class="ix-c-cb">
<?= $this->Form->checkbox('checkboxes.' . $k, ['value' => $user->id, 'id' => 'checkbox' . $k, 'class' => 'userCheckbox']); ?>
</div>
</div>
<!-- Role -->
<div class="ix-usr-role">
<div class="ix-usr-role-text"><?= h($user->role) ?></div>
</div>
</div>
<?php endforeach; ?>
Javascript function
function toggle(source) {
checkboxes = document.getElementsByClassName('userCheckbox');
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = source.checked;
}
}
It's providing a pretty clear answer right there in the top of the error:
Array to string conversion in [C:\xampp\htdocs\app\vendor
\cakephp\cakephp\src\View\Widget\CheckboxWidget.php, line 81
Simply find line 81 in your CheckboxWidget, and fix the specified problem.
If you're not sure how, simply searching "php array to string conversion error", provides many results, one of which describes your problem very specifically regarding inputs named checkboxes[].
Related
I'm working on a project which has a search form that might return many rows so I decided to manage that with Pagination (yii\data\Pagination) but in my view file, in pages different than first one, LinkPager always returns the maximum rows of result + the first one.
For example: if I put $pageSize = 1 in the first page it retrieves only one row but in second page it returns another row + the one in first page.
Here is my controller
public function actionVisualizzaAnnunci() {
$annunci1 = Annuncio::find()->where(['visibilita' => ['Pubblica', 'Privata']])->orderBy(['data_pubblicazione' => SORT_DESC]);
$annunci_registrati = Annuncio::find()->where(['settore_id' => $settori_registrato])->orderBy(['data_pubblicazione' => SORT_DESC]);
$result = $annunci1->union($annunci_registrati);
$countQuery = clone $result;
$pagination = new Pagination(['totalCount' => $countQuery->count(), 'pageSize' => 1]);
$annunci = $result->offset($pagination->offset)
->limit($pagination->limit)
->all();
}
return $this->render('visualizza-annunci', ['annunci' => $annunci, 'pagination' => $pagination]);
}
Here is my view file
foreach ($annunci as $annuncio) {
?>
<div class="risultato-ricerca">
<!--box ha link verso l'annuncio-->
<a href="dettaglio-annuncio?id=<?php echo $annuncio->id;?>">
<div class="row">
<div class="img-annuncio col-2">
<img src="<?php echo Yii::$app->homeUrl; ?>/images/icon.png" class="img-annuncio img-fluid">
</div>
<div class="annuncio col-8">
<h3> <?php echo $annuncio->titolo; ?></h3>
<h5> <?php echo $annuncio->azienda->citta; ?> | <?php echo $annuncio->data_pubblicazione; ?></h5>
<p><?php echo $annuncio->descrizione; ?></p>
<p class="right"> <?php echo $annuncio->tipo_contratto; ?> | <?php echo $annuncio->retribuzione_lorda; ?></p>
</div>
</div>
</a>
</div>
<?php } ?>
<?php
echo LinkPager::widget([
'pagination' => $pagination,
]);
?>
Can someone help me?
This issue happen since you are using UNION, so that result will be change in each page dependence of both query...
To resolve this issue:
dependence of your query, you can do join(on the same table with alais) or add another condition to retrieve the same data....(This point mean, you can write your query in another way which its return expected result.) - if thats cant done for some reason you can go to second point...
when you use union, you can use sqlDataProvider, for example:
$dataProvider = new SqlDataProvider([
'sql' => $result,// $annunci1->union($annunci_registrati);
]);
Also you can try to use custom query command and handle it manually.
I have a Yii2 form:
<?php $form = ActiveForm::begin(['id' => 'que']); ?>
<?php echo $form->field($model, 'type')
->dropDownList($questionTypes, [
'class' => 'form-control ng-pristine ng-valid ng-touched',
'prompt' => 'Select question type',
'ng-model' => 'que.type',
'ng-change' => 'addAnswerOptions(que);',
]);
?>
<?php ActiveForm::end(); ?>
On the basis of selected dropdown value, I have to add some more fields to the same form of the same model. What fields will be added, is totally depend on the dropdown value.
How can I do this?
From the information you give out here is what I would suggest.
1) Dynamic - no AJAX
Build your form with all the fields you need, just contain each "scenario" in a separate div like follows:
<?php $form = ActiveForm::begin(['id' => 'que']); ?>
<?php echo $form->field($model, 'type')
->dropDownList($questionTypes, [
'class' => 'form-control ng-pristine ng-valid ng-touched',
'prompt' => 'Select question type',
'ng-model' => 'que.type',
'ng-change' => 'addAnswerOptions(que);',
]);
?>
<div class="form-option" data-type="class" style="display:none;">
<?php
// ... fields here for case type == class
?>
</div>
<div class="form-option" data-type="prompt" style="display:none;">
<?php
// ... fields here for case type == prompt
?>
</div>
<div class="form-option" data-type="ng-model" style="display:none;">
<?php
// ... fields here for case type == ng-model
?>
</div>
<div class="form-option" data-type="ng-change" style="display:none;">
<?php
// ... fields here for case type == ng-change
?>
</div>
<?php ActiveForm::end(); ?>
Then you will want to register Javascript code to display the correct blocs depending on which dropdown option was selected.
Bellow is an example using JQuery:
$(document).ready(function(){
$('select.form-control').change(function(){
$('.form-option').hide(); // hide all options if an option is showing
var index = $('select.form-control').index();
$('div[data-type="'+index+'"]').show(); //show the correct fields
});
});
If you're going to go this way I suggest you use AJAX validation for your form. It will avoid you having to deal with a headache on page reload.
Once your form is submitted you will have to handle each case in your controller. You can either use a simple set of if() statements that check the drop down value. Or you can set your model validation scenario according to the drop down value.
The advantage of this is that it will be quicker and you will be able to take advantage of ActiveForm. the cons are that you need to know which fields you want to display for each option, and it doesn't allow you to cumulate n number of fields when you don't know how much n is.
2) Using Ajax
In the event that you want to use ajax calls to load the extra form fields you will have to make a controller/action combination that will return the fields depending on the type that you pass in the GET
This action will generate the html of the fields you want to display. Here's an example:
public function actionAjaxFields($type)
{
$html = '';
if($type == "class")
{
$html .= Html::textInput('Field1');
}
elseif($type == "prompt")
{
$html .= Html::textInput('Field2');
}
else
{
// etc...
}
return $html;
}
Note that you can also pass a user id to this method which will allow you to generate a model and use Html::activeTextInput(), however you will not be able to take advantage of ActiveForm features.
Once this is done, you should bind a function to the change event of the dropdown and use something along the lines of :
var responsePromise = $http.get("controller/ajax-fields", {params: {type: <type-from-dropdown>}});
Unfortunately I do not know much about angularjs so this is the extent of the help I can give on the javascript side of things. I'm sure there's more than enough information on google/stackoverflow about binding events and appending data to the DOM in angularjs to get you running.
Let me know if I can be of any extra help on the Yii2 side.
I'm using CakePHP's SecurityComponent. And it's very essential as it saves forms from CSRF attacks. My project has total 10-12 forms and this is my first CakePHP project. After enabling SecurityComponent I was in a bit trouble but could get rid off after some careful minutes. This is the last form of my project and seems everything is correct to me but still the form is being black holed :(. Can anybody please tell me the problem? I don't want to disable CSRF checking or SecurityComponent. Here is my view code:
<?php
echo $this->Form->create('Record');
?>
<script type="text/javascript"> var me = new MetroExam(); </script>
<div class="exam_paper">
<div class="question_box" id="q_b">
<div class="q_n_a_header">
<div class="instructions">
<b>Instructions:</b><br>
<?=$inst['value_text']; ?>
</div>
<div id="timer">Please wait</div>
</div>
<div id="q_paper">
<img id="q" style="display: none;" src="/oes/<?=$exam['path'].'?ts='.time(); ?>">
<img id="loading_img" src="/oes/img/loading.gif">
</div>
</div>
<div class="ans_box" id="a_b">
<!-- information about answer paper. !important -->
<?php
$i = 0;
//these fields are essential for evaluating ans paper
echo $this->Form->hidden('submit', array('value' => 'true'));
echo $this->Form->hidden('start_time', array('value' => ''));
echo $this->Form->hidden('end_time', array('value' => ''));
echo $this->Form->hidden('duration', array('value' => ''));
echo $this->Form->hidden('valid', array('value' => ''));
echo $this->Form->hidden('passed', array('value' => ''));
//options for all radio
$options = array(
'1' => 'A',
'2' => 'B',
'3' => 'C',
'4' => 'D'
);
if($exam['choices'] == 5){
$options['5'] = 'None';
}
$questions = (int)$exam['questions']; // 40 <= $exam['questions'] <= 100
$i = 1;
while($questions--){
echo '<div class="'.(($i%2)==1?'each_answer_even':'each_answer_odd').'" id="ans-'.$i.'">';
echo '<div class="q_number">'.($i <= 9 ? '0'.$i : $i).'</div>';
$name = 'ans'.str_pad($i, 3, '0', STR_PAD_LEFT);
$attributes = array('empty' => false, 'legend' => false, 'onclick' => 'me.answer_click('.$i.')');
echo '<div class="mcq">'.$this->Form->radio($name, $options, $attributes).'</div>';
echo '</div>';
$i++;
}
echo $this->Form->end('Submit');
?>
</div>
</div>
This is basically a MCQ exam form. Where each group has 4 or 5 radio buttons and total 40 to 100 groups in a form. I'm using CakePHP 2.4. Thanks in advance.
As per the comments, the problem appears because you are changing the hidden values of the form. The way SecurityComponent works, is that it "locks" the name of the fields, so an evildoer can't add new fields or change the values once the form is sent. But it is even more strict with the hidden values, because it locks the field name and value. So by changing it with jQuery you're blackhole-ing your own form.
There's a nice little post where I learned this, take a look at it. The author there also explains two ways of bypassing this problem. One is to disable the security for hidden fields, so the hash calculated for the token doesn't include those values... which isn't really secure...
And another solution is to modify the FormHelper, and tell it to "lock" the hidden fields names but not the values. I don't remember what version of Cake the author uses for the example, but the code given there should be practicaly the same. So with that solution, you can tell the form to not be so strict with you with an option array.
Oh, and the other option given there (this is what I normally use) (I just read it now there... I thought I figure that on my own... oh well), is to just use normal input text fields for the ones you want hidden, and add a css style like display:none.
It's up to you what you think is best. I like the css option because is simpler, and really, if someone is going to mess with my css evily (with firebug or something like that), they might just as well do it with the values of hidden fields, it doesn't require any more effort. You should take all the extra steps and verifications when handling that form submission anyway. But like I said, up to you which do you think is best for your situation.
In addition to what was already posted, here's something else what might be causing the problem: in my case, a hidden input had it's name overwritten.
$this->Form->create('ExampleModel'):
$this->Form->input('foo_bar', array(
'type' => 'hidden',
'name' => 'foo_bar',
));
As a result, the final $this->request->data had the corresponding key $this->request->data['foo_bar']. It was not within the $this->request->data['ExampleModel'] array, and that's what the problem was.
To fix the issue, I had to remove the name key from the template, making the input belong to the model's data, and then just change the controller to accept that value.
Hope this helps someone else.
Update: this would also work on a form not attached to any model, e.g.:
$this->Form->create(false, array(
'url' => '/example',
)):
I'm working on signup form and I have 2 steps to user register in some website, so step 1 is a create action and step 2 is update action on profile controller.
so my problem is how can preserve the selected country and city on validation error but it's already ?! check the image
view:
<div class="row">
<?php echo $form->labelEx($model,'countryId'); ?>
<?php echo $form->dropDownList($model, 'countryId',
CHtml::listData(SysCountry::model()->findAll(), 'id', 'name'),
array(
'empty'=>'Select country',
'ajax' => array(
'type'=>'POST', //request type
'url'=>CController::createUrl('profile/dynamiccity'), //url to call.
//Style: CController::createUrl('currentController/methodToCall')
'update'=>'#Profile_cityId', //selector to update
//'data'=>'js:javascript statement'
//leave out the data key to pass all form values through
)));
?>
<?php echo $form->error($model,'countryId'); ?>
</div>
<div class="row">
<?php echo $form->labelEx($model,'cityId'); ?>
<?php echo $form->dropDownList($model, 'cityId', array('empty'=>'Please select country first')
); ?>
<?php echo $form->error($model,'cityId'); ?>
</div>
Check this, to update the second drop down you need to use
'update'=>'#'.CHtml::activeId($model,'tracanton'),
echo CActiveForm::dropDownList
(
$model,'traprovincia',
CHtml::listData
(
CatUbicacionGeografica::model()->findAll('cat_ubigeo_nivel=? ORDER BY cat_ubigeo_descripcion', array('2') ),
'cat_ubigeo_codigo', 'cat_ubigeo_descripcion'
),
array(
'ajax'=>array(
'type' => 'POST',
'url' => CController::createUrl('tramite/updatecanton'),
'data'=>array('padre'=>'js:this.value'),
'update'=>'#'.CHtml::activeId($model,'tracanton'),
),
'prompt' => '( Seleccione Provincia )',
'style' => "width: 300px;",
// 'onchange'=>'js:$("#tracanton").focus()',
)
);
Is it javascript or PHP validation? If its the former use JS to take the values of the dropdown boxes and then repopulate (ikll post some code if its this). If its PHP then do the following on your drop downs
<select>
//-- If 'save' has been entered and validation has been caught
if (isset($_POST/GET['country'])) {
echo '<option value=". $_POST/GET['country'] . '" selected="selected">' . $_POST/GET['country'] . '</option>';
} else {
//-- if validation hasnt been actioned
}
</select>
Whilst this may not work for your code i hope you get the idea. If you can post your validation code ill be able to amend properly so it works for you.
Hello I have two questions:
(1) Is it best practice to create global custom functions in the bootstrap file? Is there a better place to store them?
(2) I am unable use the following line of code in my custom function located in my bootstrap.php file:
$url = $ajax->link ( 'Delete', array ('controller' => 'events', 'action' => 'delete', 22 ), array ('update' => 'event' ), 'Do you want to delete this event?' );
echo $url;
I receive the following error:
Notice (8): Undefined variable: ajax [APP\config\bootstrap.php, line 271]
Code
}
function testAjax () {
$url = $ajax->link ( 'Delete', array ('controller' => 'events', 'action' => 'delete', 22 ), array ('update' => 'event' ), 'Do you want to delete this event?' );
testAjax - APP\config\bootstrap.php, line 271
include - APP\views\event\queue.ctp, line 19
View::_render() - CORE\cake\libs\view\view.php, line 649
View::render() - CORE\cake\libs\view\view.php, line 372
Controller::render() - CORE\cake\libs\controller\controller.php, line 766
Dispatcher::_invoke() - CORE\cake\dispatcher.php, line 211
Dispatcher::dispatch() - CORE\cake\dispatcher.php, line 181
[main] - APP\webroot\index.php, line 91
However it works as intended if I place that same code in my view:
<a onclick=" event.returnValue = false; return false;" id="link1656170149" href="/shout/events/delete/22">Delete</a>
Please help :)
Thanks in advance!!
That depends: If it's a complete generic function that could be accessed from everywhere in your app, than yes. Otherwise I would place it in the parent-class from where you want to use it (app_model, app_controller)
$ajax is a helper class, that cannot be accessed from your bootstrap file. You would need to inculde the helper in the bootstrap, and that is the point from where it doesn't make sense to place the function there
Don't do it in bootstrap - no good place for that.
If You want to have this url on every page - put it in Your layout (http://book.cakephp.org/view/96/Layouts)