I'm using ZF2 and mysql, but the question is platform-independent. I have a data transfer object Organization that gets hydrated from an html form. OrganizationMapper has a save method that (1) gets Organization as an argument and (2) fills a couple of database tables one after another.
Suppose the 1st table gets filled ok, but the 2nd doesn't because one of the properties of Organization isn't set (not null constraint on a column). The user gets an error, but the 1st table is already filled. If he attempts to submit the form again, but this time with all html fields filled, all the tables get filled ok, but the 1st has a previous unused row.
How could I avoid this situation?
I thought of checking for empty values with if's in the mapper's save method, but it doesn't seem elegant. I know about the InputFilter validations in ZF2, but these check the user input in the form, they don't check things when the php code communicates with the database.
Any help?
The best way is to validate all the data before you start writing it to the database.
I didn't use ZF2 and this solution is actually framework-dependent, so you need to check the ZF2 docs. For example, in Yii, you just define validation rules for every field of the model, so you can ensure that your Organization contains all the data before you start saving it to the database, probably something similar is possible in Zend.
Note, that validation doesn't mean just to check for empty values, you may need to verify different things, like: "email is correct email like xxx#yyy.com", "name is not empty", "name length is more than 3 chars", "name length is less than 1000 chars" and so on.
For Yii it roughly looks like this:
class Organization extends ActiveRecord {
...
// here we define the validation rules
public function rules() {
return [
// name is required
['name', 'required'],
// check min / max length
['name', 'string', 'min' => 3, 'max' => 12],
// check if email is valid
['email', 'email']
];
}
}
Now you can do $organization->validate() to make sure everything is correct (also when you do $organization->save() the rules will be checked before saving to the database).
And one more solution to protect from the inconsistent data is to use transactions. In the case you write to multiple tables, you anyway need them, even if you validated everything. Unexpected things happen, so it is better to protect your saving code like this (pseudo-code):
$transaction->start();
try {
$table1->writeSomeData();
$table2->writeMoreData();
$transaction->commit();
} (catch Exception $e) {
$transaction->rollback();
}
Again, check your framework documentation, it probably supports this in some way.
Related
I have a template that allows adding new lines. Example: I want to add multiple products at once to a store, so I can add them all at once, through those lines that replicate themselves. The problem is that I need to validate these fields, they are all mandatory. I'm implementing so that I can walk through each one and leave it compulsory. However, I came across another problem, Laravel is telling me that my field is empty, but it is not. I would like to know how I can solve it, thank you in advance.
Here is my code to FormRequest
public function rules()
{
$rules = [];
foreach($_POST['esp'] as $key => $esp){
$name_field = "esp[" . $key . "]" . "[esprqe]"; //name of my field is complicated even as it is time based
$rules[$name_field] = 'required';
}
return $rules;
}
My template is big, no need to show.
message I receive: esp[1553533952015][esprqe]: ["The esp[1553533952015][esprqe] field is required."]
My input is not empty, I do not know what the problem is
You state the name of your field 'esprqe' is time based and complicated. This is likely the cause of your problem.
The esp['.$key.'] field is probably fine, as this field is transferred from your form. However, the value that goes into esprqe, if that is being generated in your server side code above (I don't know, you haven't provided how this field is generated), and if the rules are looking for that field to match a time-based generated field on the form... those values won't match and you will have a non-value coming into your method above.
This would very likely generate a message
["The esp[1553533952015][esprqe] field is required."]
If this is your issue, you can test by first creating a simple numbered index for the esprqe field that will be matched on both form and method. If this succeeds, you can then increase the complexity using a common generator so that the rules section knows exactly what the name of the field is. Time can't be common between form creation and rules creation - thus perhaps the reason for the failure.
You need use "dot notation" to validate attributes within an array.
$name_field = "esp.$key.esprqe";
Laravel Docs: validating arrays
I have a validation rule in one of my Table classes like this:
public function validationDefault(Validator $validator)
{
$validator
->scalar('comment')
->maxLength('comment', 3000)
->requirePresence('comment', 'create')
->notEmpty('comment');
return $validator;
}
This validates the comment field of an input and means it cannot be over 3000 characters in length. All of this is fine when working in PHP.
One part of my application uses a JavaScript character counter - it tells the user how many remaining characters they have in a field as they type. The js for this works fine, with the limit 3000 hardcoded.
However, I want to know if there's a way to avoid hardcoding this limit in my js? Because otherwise my code is not DRY as I'm defining the 3000 limit in multiple places and if it changes that's problematic to remember/update.
Is it possible to read the maxLength property directly from validationDefault for the comment field? This question concerns how to access the data defined in the Table class; I am fine with knowing how to pass it to js via ajax.
I haven't got any further code to show as I don't know if/how this is even possible.
CakePHP 3.5.13
To get validation rule value, you have to get Validator from Table, then get ValidationSet, and then ValidationRule, from which you can extract desired result. Sample controller code below:
$validator = $this->YourTable->getValidator("default");
$validationSet = $validator->field("comment");
$validationRule = $validationSet->rule("maxLength");
$result = $validationRule->get("pass");
Or, in just one line:
$result = $this->YourTable->getValidator("default")->field("comment")->rule("maxLength")->get("pass");
A returned value will be an array of additional arguments passed to validation rule, in your case it should look like:
array(1) {
[0]=>
int(3000)
}
Also, #ndm in his answer mentioned about other posibilities:
On form helper/context level it's also possible to read the schema, and support for transalting its length configuration and the validation rule value into a maxlength HTML attribute is being implemented for CakePHP 3.7
Further reading:
Getting validators from tables
Validator class
ValidationSet class
ValidationRule class
In my controllers that Gii creates it is common to see the following:
if($model->load(Yii::$app->request->post()) && $model->save()){
//.....do something such as redirect after save....//
}else
{
//.....render the form in initial state.....//
}
This works to test whether a POST is sent from my form && the model that I am specifying has saved the posted information (as I understand it).
I've done this similarly in controllers that I have created myself but in some situations this conditional gets bypassed because one or both of these conditions is failing and the form simply gets rendered in the initial state after I have submitted the form and I can see the POST going over the network.
Can someone explain why this conditional would fail? I believe the problem is with the 'Yii::$app->request->post()' because I have removed the '$model->save()' piece to test and it still bypasses the conditional.
Example code where it fails in my controller:
public function actionFreqopts()
{
$join = new FreqSubtypeJoin();
$options = new Frequency();
$model = new CreateCrystal();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
$model->insertFreqopts();
return $this->redirect(['fieldmap', 'id' => $join->id]);
} else {
return $this->render('freqopts', ['join' => $join, 'options' => $options]);
}
}
My initial thought was that I'm not specifying the correct "$model" in that I'm trying to save the posted data to FreqSubtypeJoin() in this case and the $model is CreateCrystal(); however, even when I change the model in this conditional it still fails. It would be helpful if someone could briefly explain what the method 'load' is actually doing in layman's terms if possible.
The load() method of Model class is basically populating the model with data from the user, e.g. a post query.
To do this it firstly loads your array of data in a form that matches how Yii stores your record. It assumes that the data you are trying to load is in the form
_POST['Model name']['attribute name']
This is the first thing to check, and, as long as your _POST data is actually getting to the controller, is often where load() fails, especially if you've set your own field names in the form. This is why if you change the model, the model will not load.
It then check to see what attributes can be massively assigned. This just means whether the attributes can be assigned en-mass, like in the $model->load() way, or whether they have to be set one at a time, like in
$model->title = "Some title";
To decide whether or not an attribute can be massively assigned, Yii looks at your validation rules and your scenarios. It doesn't validate them yet, but if there is a validation rule present for that attribute, in that scenario, then it assumes it can be massively assigned.
So, the next things to check is scenarios. If you've not set any, or haven't used them, then there should be no problem here. Yii will use the default scenario which contains all the attributes that you have validation rules for. If you have used scenarios, then Yii will only allow you to load the attributes that you have declared in your scenario.
The next thing to check is your validation rules. Yii will only allow you to massively assign attributes that have associated rules.
These last two will not usually cause load() to fail, you will just get an incomplete model, so if your model is not loading then I'd suggest looking at the way the data is being submitted from the form and check the array of _POST data being sent. Make sure it has the form I suggested above.
I hope this helps!
I am trying to setup a simple Restfull api using cakephp.
I followed the documentation from the Cakephp site.
But I am encountering the following issue.
I am using Postman plugin to test the Api calls.
I have a table called 'Categories' and in its controller have an action add().
public function add() {
if ($this->request->is('post')) {
$this->Category->create();
if ($this->Category->save($this->data)) {
$message = 'Saved';
}
else {
$message = 'Error';
}
$this->set(array(
'message' => $message,
'_serialize' => array('message')
));
}
}
and in db, I have Category table with schema (id (int 11, PK, A_I), name(varchar(40)), created (datetime), modified(datetime)).
So from postman I send POST requests to http://myProject/categories.json.
From my understanding when i send key:name and value: test, it must save into the database, which works fine. But I must get error when I replace the "key" with something other than name. i.e for exmaple key: name123 and value: test2, But this data too is getting saved in the db without any error except for the name field. i.e it is saving (id:some value, name:"", created:somevalue, modified:someValue)
I dont understand how. Any help will be greatly appreciated.
You will need to provide more info because what you say doesn't make sense. For example what do your posted data look like? Is there any kind of validation in the model? How do you expect a key/value pair to be stored in only one field (specifically name) in the DB?
What happens now is that you tell Cake to save the supplied data ($this->Category->save($this->data)) although you don't check (via cake's validation rules in the model or any other means) that it contains useful arguments and especially Category.name.
As computers will just do what you tell them to do and not what you imply or have in mind, it goes on and sends the calculated created/modified fields to the DB which in turn saves them with the autoincremented ID. Since name doesn't have a UNIQUE or NOT NULL field condition in the DB it is saved as NULL or empty string.
I've done quite a few Lithium tutorials (links below in case they help someone else, and also to show I've done my homework:) and I understand the most basic parts of creating models, views, controllers and using MVC to create a DB record based on form input.
However, I'm new to MVC for webapps and Lithium, and I'm not sure how I should write my code in more complicated situations. This is a general question, but two specific validation questions that I have are:
How should I validate date data submitted from the form?
How should I check that the two user email fields have the same value?
I would be very grateful for any help with these questions, and concrete examples like this will also really help me understand how to do good MVC coding in other situations as well!
Date entry - validating data split across multiple form inputs
For UI reasons, the sign up form asks users to enter their DOB in three fields:
<?=$this->form->field('birthday', array('type' => 'select', 'list' => array(/*...*/))); ?>
<?=$this->form->field('birthmonth', array('type' => 'select', 'list' => array(/*...*/))); ?>
<?=$this->form->field('birthyear', array('type' => 'select', 'list' => array(/*...*/))); ?>
What is the best way to validate this server-side? I think I should take advantage of the automagic validation, but I'm not sure of the best way do that for a set of variables that aren't really part of the Model. E.g.:
Should I post-process the $this->request->data in UsersController? E.g. modify $this->request->data inside UsersController before passing it to Users::create.
Should I pull the form fields out of $this->request->data and use a static call to Validator::isDate inside UsersController?
Is there a way to write a validation rule in the model for combinations of form variables that aren't part of the model?
should I override Users::create and do all the extra validation and post-processing there?
All of these seem like they could work, although some seem a little bit ugly and I don't know which ones could cause major problems for me in the future.
[EDIT: Closely related to this is the problem of combining the three form fields into a single field to be saved in the model]
Email entry - checking two form fields are identical, but only storing one
For common sense/common practice, the sign up form asks users to specify their email address twice:
<?=$this->form->field('email_address'); ?>
<?=$this->form->field('verify_email_address'); ?>
How can I write an automagic validation rule that checks these two form fields have the same value, but only saves email_address to the database?
This feels like it's pretty much the same question as the above one because the list of possible answers that I can think of is the same - so I'm submitting this as one question, but I'd really appreciate your help with both parts, as I think the solution to this one is going to be subtle and different and equally enlightening!
[EDIT: Closely related to this is the problem of not storing verify_email_address into my model and DB]
Some background reading on Lithium
I've read others, but these three tutorials got me to where I am with users and sign up forms now...
Blog tutorial
Extended blog tutorial
MySQL blog tutorial
Some other StackOverflow questions on closely related topics (but not answering it and also not Lithium-specific)
One answer to this question suggests creating a separate controller (and model and...?) - it doesn't feel very "Lithium" to me, and I'm worried it could be fragile/easily buggy as well
This wonderful story convinced me I was right to be worried about putting it in the controller, but I'm not sure what a good solution would be
This one on views makes me think I should put it in the model somehow, but I don't know the best way to do this in Lithium (see my bulleted list under Date Entry above)
And this Scribd presentation asked the question I'm hoping to answer on the last page... whereupon it stopped without answering it!
NB: CakePHP-style answers are fine too. I don't know it, but it's similar and I'm sure I can translate from it if I need to!
I'd recommend doing this in the Model rather than the Controller - that way it happens no matter where you do the save from.
For the date field issue, in your model, override the save() method and handle converting the multiple fields in the data to one date field before calling parent::save to do the actual saving. Any advanced manipulation can happen there.
The technique described in your comment of using a hidden form field to get error messages to display sounds pretty good.
For comparing that two email fields are equal, I'd recommend defining a custom validator. You can do this in your bootstrap using Validator::add.
use lithium\util\Validator;
use InvalidArgumentException;
Validator::add('match', function($value, $format = null, array $options = array()) {
$options += array(
'against' => '',
'values' => array()
);
extract($options);
if (array_key_exists($against, $values)) {
return $values[$against] == $value;
}
return false;
});
Then in your model:
public $validates = array(
"email" => array(
"match",
"message" => "Please re-type your email address.",
"against" => "email2"
)
);
Edit: Per the comments, here's a way to do custom rule validation in a controller:
public function save() {
$entity = MyModel::create($this->request->data);
$rules = array(
"email" => array(
"match",
"message" => "Please re-type your email address.",
"against" => "email2"
)
);
if (!$entity->validates($rules)) {
return compact('entity');
}
// if your model defines a `$_schema` and sets `$_meta = array('locked' => true)`
// then any fields not in the schema will not be saved to the db
// here's another way using the `'whitelist'` param
$blacklist = array('email2', 'some', 'other', 'fields');
$whitelist = array_keys($entity->data());
$whitelist = array_diff($whitelist, $blacklist);
if ($entity->save(null, compact('whitelist'))) {
$this->redirect(
array("Controller::view", "args" => array($entity->_id)),
array('exit' => true)
);
}
return compact('entity');
}
An advantage of setting the data to the entity is that it will be automatically prefilled in your form if there's a validation error.