CakePHP 3 - Form data format for patchEntities() - forms

My question is pretty straightforward: How should form data look like (e.i. what should the name keys look like) to patch multiple entities using patchEntities()?
I have read
Cookbook > Database Access & ORM > Saving Data > Converting Multiple Records, but it does not explicitly mention how to use it.
This structure works for newEntities():
$data = [
'0' => ['field1' => '...', /* ... */],
'1' => ['field1' => '...', /* ... */],
'2' => ['field1' => '...', /* ... */]
];
With a form like:
<?= $this->Form->input('0.field1', /* ... */) ?>
<?= $this->Form->input('1.field1', /* ... */) ?>
...
However, the same structure but with 'id.field1' does not make any changes to entities in patchEntities().

The form data should look the same, with the exception that it should include the records primary keys, so that the marshaller can map the data on the respective records. The leading number there in your form inputs isn't a primary key (id), but just the resulting array index.
The docs could use a little update there to include patchEntities() too, right now there's only a short section hidden in the "Patching HasMany and BelongsToMany" section.
Cookbook > Database Access & ORM > Saving Data > Patching HasMany and BelongsToMany
So the form should look something like:
<?= $this->Form->input('0.id', /* ... */) ?>
<?= $this->Form->input('0.field1', /* ... */) ?>
<?= $this->Form->input('1.id', /* ... */) ?>
<?= $this->Form->input('1.field1', /* ... */) ?>
<!-- ... -->
resulting in a dataset like:
$data = [
'0' => ['id' => '...', 'field1' => '...', /* ... */],
'1' => ['id' => '...', 'field1' => '...', /* ... */],
// ...
];
which can then be patched on the given entities:
$original = $this->Table->find()->toArray();
$patched = $this->Table->patchEntities($original, $data);

Related

How to make the value (to be read) of form field widget in yii2 different from what users see in the input text box

I am modifying a system already developed in yii2 framework.
There is a functionality where a user views a list of customers and against each record there is a button for editing. When clicked, a form is displayed with values automatically filled in for the customer against whom the 'edit' button was clicked.
Now, the is a field for address which is filled with the database record id of the address instead of the name of the location. The id is a number, and the field was validated to check that the value is a number when the 'Update' button is clicked.
Problem:
Instead of filling the field with the number (i.e the id) I am requested by users to fill that field with the name of the location which is not a number and therefore will violate the validation rule.
I want to be able to make the value of the input field (i.e the value attribute) to be the id -a number so that it does not violate the set rule - but display the name of the location in the text input so that the users have a readable 'value' to look at.
How can I achieve this?
CODE
There was this:
<div class="col-sm-6">
<?= $form->field($model, 'village')->widget(\kartik\widgets\Select2::classname(), [
'options' => ['value' =>$model->village],
'pluginOptions' => [
'allowClear' => true
],
]); ?>
</div>
The value in $model->village is the id of the village. This is what users do not want.
I tried doing this:
<div class="col-sm-6">
<?php
$villageName = Yii::$app->db->createCommand('SELECT village_name FROM v_village WHERE id='.$model->village)->queryOne();
$streetName = Yii::$app->db->createCommand('SELECT street FROM v_street WHERE street_id='.$model->street)->queryOne();
?>
<?= $form->field($model, 'village')->widget(\kartik\widgets\Select2::classname(), [
'options' => ['value' => $villageName['village'], 'id' => $model->village],
'pluginOptions' => [
'allowClear' => true
],
]); ?>
</div>
But that violates the validation rule and the error I get says 'Village must be a number'
You have to set data array of your select.
<div class="col-sm-6">
<?php
$villageName = Yii::$app->db->createCommand('SELECT village_name FROM v_village WHERE id='.$model->village)->queryOne();
$streetName = Yii::$app->db->createCommand('SELECT street FROM v_street WHERE street_id='.$model->street)->queryOne();
?>
<?= $form->field($model, 'village')->widget(\kartik\widgets\Select2::classname(), [
'data' => [$model->village => $villageName['village']],
'pluginOptions' => [
'allowClear' => true //are you sure you want to be able to clear the option? If you save it empty the query above will break
],
]); ?>
</div>
I'm not sure what is your purpose, but if you have only this value you can just show it like text.
If you want to be able to change the village you code must be like this:
<?= $form->field($model, 'village')->widget(\kartik\widgets\Select2::classname(), [
'data' => ArrayHelper::map(Village::find()->all(), 'id', 'village_name'),
'pluginOptions' => [
'allowClear' => true //are you sure you want to be able to clear the option? If you save it empty the query above will break
],
]); ?>
Also you can remove the query and set data like this:
'data' => ArrayHelper::map(Village::find()->where(['id' => $model->village])->all(), 'id', 'village_name')

Troubleshooting CakePHP form submission

I recently set up the ability to tag posts on my site. I had everything working fine. Then as I was wrapping up I tested all my admin side forms again. The Add Tag form no longer does anything. It doesn't even flash an error or redirect after submission. The page just reloads at the same URL. The only changes to the site I have made since initial testing was move the forms to the admin side of the dev site. Here is some code to hopefully reveal what the mystery is. Also my edit tag form is doing similar thing. It has no flash message but redirects back to the index, like its supposed to but with no changes made to the tag. Ill include the edit code as well.
Add.ctp in src/Template/Admin/Tags/Add.ctp
<div class="tags form large-9 medium-8 columns content">
<?= $this->Form->create($tag) ?>
<div class="form-group">
<fieldset>
<h1 class="page-header">New Tag</h1>
<?php
echo $this->Form->input('name', ['class' => 'form-control']);
?>
</fieldset>
</div>
<?= $this->Form->button(__('Submit'), ['class' => 'btn btn-primary']) ?>
<?= $this->Form->end() ?>
</div>
Here is my Add funciton in my TagsController:
public function add()
{
$this->viewBuilder()->layout('admin');
$tag = $this->Tags->newEntity();
if ($this->request->is('post')) {
$tag = $this->Tags->patchEntity($tag, $this->request->data);
if ($this->Tags->save($tag)) {
$this->Flash->success(__('The tag has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The tag could not be saved. Please, try again.'));
}
$this->set(compact('tag'));
$this->set('_serialize', ['tag']);
}
Here is my Edit funciton in my TagsController:
public function edit($id = null)
{
$this->viewBuilder()->layout('admin');
$tag = $this->Tags->get($id, [
'contain' => []
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$tag = $this->Tags->patchEntity($tag, $this->request->data);
if ($this->Tags->save($tag)) {
$this->Flash->success(__('The tag has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The tag could not be saved. Please, try again.'));
}
$this->set(compact('tag'));
$this->set('_serialize', ['tag']);
}
Edit.ctp in src/Template/Admin/Tags/Edit.ctp
<div class="tags form large-9 medium-8 columns content">
<?= $this->Form->create($tag) ?>
<div class="form-group">
<fieldset>
<h1 class="page-header">Edit Tag</h1>
<?php
echo $this->Form->input('name', array('class' => 'form-control'));
?>
</fieldset>
</div>
<?= $this->Form->button(__('Submit'), ['class' => 'btn btn-primary']) ?>
<?= $this->Form->end() ?>
</div>
Just as a side note. I started getting errors when creating a new post as well.
General error: 1364 Field 'section_id' doesn't have a default value
I did go into my DB and give the field a default value. But then when I fill out the form for a new post again, the error just moves to the next table column. I am assuming they are some how related since they popped up at the same time and because tags and posts are related to each other.
TagsTable:
class TagsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('tags');
$this->displayField('name');
$this->primaryKey('id');
$this->hasMany('PostsTags', [
'foreignKey' => 'tag_id'
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->integer('id')
->allowEmpty('id', 'create');
$validator
->requirePresence('name', 'create')
->notEmpty('name');
return $validator;
}
}
Tags Entity:
class Tag extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* #var array
*/
protected $_accessible = [
'*' => false,
'id' => false
];
}
When I place <?php debug($tag); ?> into my add.ctp view this is the out put it gives me:
object(App\Model\Entity\Tag) {
'[new]' => true,
'[accessible]' => [],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tags'
}
Again, in question always post debug pathEntity output, in your case debug($tag), also Tag Entity, your validation code, and how looks your db tags table.
Answer:
General error: 1364 Field 'section_id' doesn't have a default value
This means that you have not passed a value for this field.
You can change that table field to accept null or empty value and/or set default if not passed from application, or make validation in your TagsTable to be sure if submitted data valid before send to db.
After question updated:
protected $_accessible = [
'*' => false, <---- should be true
'id' => false
];
This means that all fields except id are accessible

Yii2 form input field counts with value from db

I do my project in yii2.
I have form which get data from db to dropdown list(using kartik depdtop widget).
First field is "type_of_goods", depending on "type_of_goods" customer receive "goods".
After they are two text input field with "goods_amount" and "total_cost" of goods depends on "goods_amount" (goods_amount * price).
Customer inputs goods amount he wants to buy, or money amount to which he wants to buy and js script shows him value in another field in both cases.
Price value is in goods table in DB.
Can I get goods_id or some information about goods from "goods"-field(to perform db query and get price and put it into js function), or maybe even price to put it into js script which do things that I wrote above.
How can I realize it? Is it right way to do it?
Code:
View
<?php $form = ActiveForm::begin([
'options' => [
'class' => 'form-horizontal col-lg-11',
'enctype' => 'multipart/form-data'
],
]); ?>
<?= $form->field($type_of_goods, 'id')
->dropDownList(
ArrayHelper::map(Type_of_goods::find()->all(), 'id', 'name'),
['id'=>'type_of_goods_id']
);
?>
<?= $form->field($goods, 'id')->widget(DepDrop::classname(), [
'options' => ['id' => 'id'],
'pluginOptions' => [
'depends' => ['type_of_goods_id'],
'placeholder' => 'Choose your goods',
'url' => \yii\helpers\Url::to(['goods/goods-dep-type'])
]
]);
?>
<?= $form->field($goods, 'price')->textInput();
?>
<?= $form->field($order, 'amount')->textInput();
?>
<?php ActiveForm::end(); ?>
Controller:
public function actionGoodsDepType()
{
$out = [];
if (isset($_POST['depdrop_parents'])) {
$parents = $_POST['depdrop_parents'];
if ($parents != null) {
$game_id = $parents[0];
$out = \app\models\Goods::gelGoodsList($goods_type_id);
echo Json::encode(['output' => $out, 'selected' => '']);
return;
}
}
echo Json::encode(['output' => '', 'selected' => '']);
}
Model:
public static function gelGoodsList($type_id)
{
$query = self::find()
->where(['type_id' => $type_id])
->select(['id', 'name'])->asArray()->all();
return $query;
}
public static function getPrice($id)
{
$query = self::find()
->where(['id' => $id])
->select(['price'])->asArray()->one();
return $query;
}
You would need to create an AJAX request each time user inputs new value. This might work well (place at the bottom of the view file):
$this->registerJs("
$('#Type_of_goods-price').on('change', function() { // Should be ID of a field where user writes something
$.post('index.php?r=controller/get-price', { // Controller name and action
input_price: $('#Type_of_goods-price').val() // We need to pass parameter 'input_price'
}, function(data) {
total_price = $.parseJSON(data)['result']; // Let's say, controller returns: json_encode(['result' => $totalPrice]);
$('#Type_of_goods-total').value = total_price; // Assign returned value to a different field that displays total amount of price
}
});
");
You only need to set correct elements' correct IDs and write a method in controller (using this example, it would be controller controller name and actionGetPrice method name).

ZF: Form array field - how to display values in the view correctly

Let's say I have a Zend_Form form that has a few text fields, e.g:
$form = new Zend_Form();
$form->addElement('text', 'name', array(
'required' => true,
'isArray' => true,
'filters' => array( /* ... */ ),
'validators' => array( /* ... */ ),
));
$form->addElement('text', 'surname', array(
'required' => true,
'isArray' => true,
'filters' => array( /* ... */ ),
'validators' => array( /* ... */ ),
));
After rendering it I have following HTML markup (simplified):
<div id="people">
<div class="person">
<input type="text" name="name[]" />
<input type="text" name="surname[]" />
</div>
</div>
Now I want to have the ability to add as many people as I want. I create a "+" button that in Javascript appends next div.person to the container. Before I submit the form, I have for example 5 names and 5 surnames, posted to the server as arrays. Everything is fine unless somebody puts the value in the field that does not validate. Then the whole form validation fails and when I want to display the form again (with errors) I see the PHP Warning:
htmlspecialchars() expects parameter 1 to be string, array given
Which is more or less described in ticket: http://framework.zend.com/issues/browse/ZF-8112
However, I came up with a not-very-elegant solution. What I wanted to achieve:
have all fields and values rendered again in the view
have error messages only next to the fields that contained bad values
Here is my solution (view script):
<div id="people">
<?php
$names = $form->name->getValue(); // will have an array here if the form were submitted
$surnames= $form->surname->getValue();
// only if the form were submitted we need to validate fields' values
// and display errors next to them; otherwise when user enter the page
// and render the form for the first time - he would see Required validator
// errors
$needsValidation = is_array($names) || is_array($surnames);
// print empty fields when the form is displayed the first time
if(!is_array($names))$names= array('');
if(!is_array($surnames))$surnames= array('');
// display all fields!
foreach($names as $index => $name):
$surname = $surnames[$index];
// validate value if needed
if($needsValidation){
$form->name->isValid($name);
$form->surname->isValid($surname);
}
?>
<div class="person">
<?=$form->name->setValue($name); // display field with error if did not pass the validation ?>
<?=$form->surname->setValue($surname);?>
</div>
<?php endforeach; ?>
</div>
The code work, but I want to know if there is an appropriate, more comfortable way to do this? I often hit this problem when there is a need for a more dynamic - multivalue forms and have not find better solution for a long time.
Having no better idea, I have created a view helper that handles the logic presented above. It can be found here.
If the helper is available in the view, it can be used in the following way (with the form from the question):
<?=
$this->formArrayElements(
array($form->name, $form->surname),
'partials/name_surname.phtml'
);
?>
The contents of the application/views/partials/name_surname.phtml partial view are:
<div class="person">
<?= $this->name ?>
<?= $this->surname ?>
</div>
The fields are rendered according to the posted form and validation messages are shown only next to the values that failed validation.
The helper's code is far from perfect (I just rewrote the idea from the question) but is easy to use and can be considered as good starting point.

ZEND form elements in a table containing also data from the database

Hi there:) i've got a problem with decorators and form which would be in table and in this table want to have also data from database... I dont have any idea how to do this to have a structure like something below, lets say
<table>
<tr>
<td><?php echo array[0]['name']?>
//and here input from zend form
<td>
<select name='foo' id='bar'>
<option value='something'>Foo</option>
<option value='something2'>Foo2</option>
</select>
</td>
</tr>
</table>
Ofcourse tr will be more and generated with foreach or some loop.
I have something like this:
<?php
class EditArticles_Form_EditArticles extends Zend_Form
{
protected $uid;
public function render()
{
/* Form Elements & Other Definitions Here ... */
$this->setName('editarticles');
$data = new EditArticles_Model_DbTable_EditArticlesModel();
$datadata = $data->GetArticlesToEdit($this->getUid()); //here is my data from db
for ($i=0;$i<count($datadata);$i++)
{
$do = new Zend_Form_Element_Select(''.$i);
$do->addMultiOption('0', 'Aktywny');
$do->addMultiOption('1', 'Nieaktywny');
$this->addElements(array($do));
}
$submit = new Zend_Form_Element_Submit('updateart');
$this->addElement($submit);
//and here are decorators for array, and i would like to have in this table also data from array containing data from database
$this->addDecorators(array(
'FormElements',
array('HtmlTag', array('tag' => 'table', 'id' => 'aaaa', 'style' => 'width:500px;')), 'Form',
));
$this->setElementDecorators(array(
'ViewHelper',
array( array('data' => 'HtmlTag'), array('tag' => 'td', 'style' => 'width:200px;')),
array('Label', array('tag' => 'td')),
array(array('row' => 'HtmlTag'), array('tag' => 'tr'))
),
//wykluczenie submita z overrida stulu
array('submit'), false);
return parent::render();
}
//setting user id for get content from db
public function setUid($uid) {
$this->uid = $uid;
return $this;
}
public function getUid() {
return $this->uid;
}
}
?>
output of code above is something like this: (in red marked where i would like to have that selects from form. In this image the table with data is an other table generated in phtml, but i would like to generate that table by form od just insert only the form elements to that table generated in phtml view).
http://img14.imageshack.us/img14/9973/clipboard01pw.png
Something found here:
Zend_Form: Database records in HTML table with checkboxes
but i dont know how to start with that...
Several comments:
Typically, adding elements to the form is done in init(), rather than render().
If a consumer object (this is this case, the form) needs a dependency (in this case, the article model) to do its work, it is often helpful to explicitly provide the dependency to the consumer, either in the consumer's constructor or via setter method (ex: $form->setArticleModel($model)). This makes it easier to mock the model when testing the form and clearly illustrates the form's dependence on the model.
Re: rendering other content in the form via decorators: Maybe, take a look at the AnyMarkup decorator. It looks like (sorry, can't fully understand the Polish) you want a select box on each row you output. So, you get your rows using the model, loop through the rows, creating your select box on each row. When you assign decorators to the select element - ViewHelper, Errors, probably an HtmlTag decorator to wrap it in a <td> - you also add the AnyMarkup decorator to prepend the a bunch of <td>'s containing your row data, finally wrapping the whole row in <tr>.
Perhaps something like this (not fully tested, just to give the idea):
class EditArticles_Form_EditArticles extends Zend_Form
{
protected $model;
public function __construct($model)
{
$this->model = $model;
parent::__construct();
}
public function init()
{
$rows = $this->model->GetArticlesToEdit($this->getUid());
$numRows = count($rows);
for ($i = 0; $i < $numRows; $i++) {
$do = new Zend_Form_Element_Select('myselect' . $i);
$do->addMultiOption('0', 'Aktywny');
$do->addMultiOption('1', 'Nieaktywny');
$do->setDecorators(array(
'ViewHelper',
array(array('cell' => 'HtmlTag'), array(
'tag' => 'td'
)),
array('AnyMarkup', array(
'markup' => $this->_getMarkupForRow($i, $row),
'placement' => 'PREPEND',
)),
array(array('row' => 'HtmlTag'), array(
'tag' => 'tr'
)),
));
$this->addElement($do);
}
}
protected function _getMarkupForRow($i, $row)
{
return '<td>' . $i . '</td>' .
'<td>' . $row['nazwa'] . '</td>' .
'<td>' . $row['typ'] . '</td>' .
'<td>' . $row['rozmiar'] . '</td>';
}
}
A final note: Remember to register an element decorator prefix path as follows (in the form, probably in init()):
$this->addElementPrefixPath('My_Decorator', 'My/Decorator', self::DECORATOR);
This allows the element to resolve the short name AnyMarkup into a full classname My_Decorator_AnyMarkup.