Symfony form validation of integer field - forms

I am trying to use the type validation rule with integer and it fails with some warning.
Here is my form
class BusinessType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('business_number', 'integer', array(
'required' => false,
));
}
}
Here is my validation rule
My\Bundle\Entity\Business:
properties:
business_number:
- Type:
type: integer
So nothing extravagant!
But I get the following error
Uncaught PHP Exception Symfony\Component\Debug\Exception\ContextErrorException: "Warning: NumberFormatter::parse(): Number parsing failed"
I already found a work around here, but it doesn't feel right to do that. I will if there is no other solution but I prefer to avoid it.
I know it was a known bug in earlier version of Symfony but it is supposed to be fix. See here.
So is there a way I can use the type validation? And if so, what am I missing?
Edit 1
I am using Symfony 2.6.6
Edit 2
If my value starts with numbers (like 123dd), I have the following error message, even if I customized my error message
This value is not valid.
But if my value starts with something else, I have the error fore-mentioned.
Edit 3
The longest value I need to store is 9 digits long. So integer should work properly.
Edit 4
Here is the bug report

The problem is that the integer and/or number Symfony Form Type utilizes the Symfony\Component\Intl\NumberFormatter\NumberFormatter::parse method before storing the value to the Form. The contents of the method are as such (as of Symfony 2.6.6):
public function parse($value, $type = self::TYPE_DOUBLE, &$position = 0)
{
if ($type == self::TYPE_DEFAULT || $type == self::TYPE_CURRENCY) {
trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING);
return false;
}
preg_match('/^([^0-9\-\.]{0,})(.*)/', $value, $matches);
// Any string before the numeric value causes error in the parsing
if (isset($matches[1]) && !empty($matches[1])) {
IntlGlobals::setError(IntlGlobals::U_PARSE_ERROR, 'Number parsing failed');
$this->errorCode = IntlGlobals::getErrorCode();
$this->errorMessage = IntlGlobals::getErrorMessage();
$position = 0;
return false;
}
preg_match('/^[0-9\-\.\,]*/', $value, $matches);
$value = preg_replace('/[^0-9\.\-]/', '', $matches[0]);
$value = $this->convertValueDataType($value, $type);
$position = strlen($matches[0]);
// behave like the intl extension
$this->resetError();
return $value;
}
Notably this part:
preg_match('/^([^0-9\-\.]{0,})(.*)/', $value, $matches);
// Any string before the numeric value causes error in the parsing
if (isset($matches[1]) && !empty($matches[1])) {
IntlGlobals::setError(IntlGlobals::U_PARSE_ERROR, 'Number parsing failed');
// ...
will cause any malformed entry with a string at the start to throw an Exception.
Unfortunately, changing the validation rules will do nothing as this parsing is run before validation occurs.
Your only workaround will be the one you've linked, and to submit a bug report until the problem is fixed. The current master branch doesn't have an update to this file and it's unclear if the problem was solved elsewhere (further research would be required).
Front-end validation could also help (for example, the built-in validation for HTML5 number and integer types will cause most browsers to stop you before submitting to Symfony).

Related

Phalcon form validation messages

There are some misunderstandings about how the messaging works for the Forms in Phalcon. Say we have a form and trying to add some extended error message for one of fields named 'code' in controller:
$form = new SampleForm();
Implementation of SampleForm is done in a corresponding class through initialize and the code element is $code = new \Phalcon\Forms\Element\Text('code');
The next code adds the message :
$form->get('code')->appendMessage(new \Phalcon\Validation\Message("The Code desn\'t exist or not valid"));
but trying to receive this message like
$form->getMessagesFor('code')
gives me nothing (the dump):
Phalcon\Validation\Message\Group Object
(
[_position:protected] =>
[_messages:protected] =>
)
Another attempt via
$form->get('code')->getMessages()
gives (the dump):
Phalcon\Validation\Message\Group Object
(
[_position:protected] =>
[_messages:protected] => Array
(
[0] => Phalcon\Validation\Message Object
(
[_type:protected] =>
[_message:protected] => The Code desn\'t exist or not valid
[_field:protected] =>
[_code:protected] => 0
)
)
)
My question: what am I doing wrong and why $form->[get/has]MessagesFor($name) doesn't work as expected?
I'm sorry to tell you, form handling is really poorly implemented and messed up in Phalcon, as I can see now, after a few months work.
In this case, you have multiple getMessage functions and they DO NOT return the same value. Even some of them returns a reference and some of them returns a copy of the error messages, and if you are appending messages to the copy, they won't be available elsewhere through the getMessage functions.
This even changed form version 1.2 to 1.3, we had a hard time figuring out which functions should we use. If you like, you could check out the C code behind this, in the Phalcon repository, it has been a great help for me to figure out why things don't work the way I expected.
My advices is, print the values of getMessage functions: Form::getMessages(), Form::getMessagesFor(), Form::get('element_name')->getMessages(). Then try appending new messages to them and print them again. See which one contains your messages.
I ended up getting messages for Phalcon's built in validation classes by $form->get('email')->getMessages() and getting my own added in the controller by $form->getMessagesFor('email'). I am still searching for a solution to get all the messages in one place.
Serin is right, messages are saved into different objects, so i came up with getting all of those like this
<?php foreach ($form as $element) {
//Get any generated messages for the current element
foreach ([$form->getMessagesFor($element->getName()), $element->getMessages()] as $messages) {
if (count($messages)) {
//Print each element
foreach ($messages as $message) {
echo $message;
}
}
}
}?>
Let's hope easier method will be added in nearest future:

using unset in CakePHP MongoDB

I am using Ichikawa CakePHP MongoDB plugin. I have a problem in using unset in it. I have tried the command in shell:
db.patents.update({}, {$unset : {"lv.2" : 1 }},{'multi':true})
db.patents.update({},{$pull:{pid:"2"}},{'multi':true})
These are working fine.
But when I am converting them to CakePHP command as follows:
$this->Detail->updateAll(array('$unset'=>array('lv.2'=>1,array('multi'=>true))));
Then it doesn't work and gives error:
MongoCollection::update(): expects parameter 1 to be an array or object, boolean given
Can anyone help me to figure out the problem.
Thanks.
There are no conditions
The error message means that the query being generated is the equivalent of:
db.details.update(true
This can be confirmed by checking the query log (easy if you're using debug kit).
How is that happening
The second parameter for model updateAll is missing, which means it will have the default:
public function updateAll($fields, $conditions = true) {
^
return $this->getDataSource()->update($this, $fields, null, $conditions);
}
Therefore in the mongodb datasource class - the conditions passed are true:
public function updateAll(&$Model, $fields = null, $conditions = null) {
^
As a consequence, the resultant update statement has true as the first parameter, not an array.
Correct syntax
The correct syntax for such a query is:
$this->Detail->updateAll(
array('$unset'=>array('lv.2'=>1))
array() # <- do not omit this
);
Note that it's not necessary to specify 'multi'=>true as the datasource does that for you, especially not in the fields argument.

Yii RBAC make Users update profile by himself

I'm trying to do this with mongodbauthmanager. I'm follow step by step in Usage section but finally i'm getting PHP warning: Illegal offset type. I had posted this question at Yii Extension before clone to SO:
Please tell me what is wrong?
1// Config
'authManager'=>array(
'class' =>'CMongoDbAuthManager',
'showErrors' => true,
),
2// Create auth items in db
$auth = new CMongoDbAuthManager();
$bizRule = 'return Yii::app()->user->id==$params["User"]->_id;';
$auth->createTask('updateSelf', 'update own information', $bizRule);
//I had tried with $auth->createOperation() but they has the same error
$role = $auth->createRole('user');
$role->addChild('updateSelf');
$auth->save();
and here is result in db
result in db http://i.minus.com/iIpXoBlDxaEfo.png
**3// Checking access in controller ** - UPDATE CODE AND ERROR
public function actionUpdate($id)
{
$model=$this->loadModel($id);
$params = array('User'=>$model);
if (!Yii::app()->user->checkAccess('updateSelf', Yii::app()->user->id,$params) )
{
throw new CHttpException(403, 'You are not authorized to perform this action');
}
//another statement ...
}
4// Getting error:
Fatal error : Cannot use object of type MongoId as array in F:\Data\03. Lab\www\yii\framework\web\auth\CAuthManager.php(150) : eval()'d code on line 1
RESOLVED PROBLEM
Base-on the answer of #Willem Renzema, I resolve my problem. Now, I update here and hope it useful for someone have this error.
0// First, config authManager with defaultRoles
'authManager'=>array(
'class'=>'CMongoDbAuthManager',
'showErrors' => true,
'defaultRoles'=> array('user'),//important, this line help we don't need assign role for every user manually
),
1// Fix save id in UserIdentity class
class UserIdentity extends CUserIdentity
{
private $_id;
//...
public function authenticate()
{
//...
$this->_id = (string)$user->_id;//force $this save _id by string, not MongoId object
//...
}
//...
}
2// Fix $bizrule in authe items
($bizrule will run by eval() in checkAccess)
//use _id as string, not MongoId object
$bizRule = 'return Yii::app()->user->id==(string)$params["User"]->_id;';
3// And user checkAccess to authorization
public function actionUpdate($id){
/**
* #var User $model
*/
$model=$this->loadModel($id);
$params = array('User'=>$model);
if (!Yii::app()->user->checkAccess('updateSelf', $params) )
{
throw new CHttpException(403, 'You are not authorized to perform this action');
}
//...
}
4// Done, now we can use checkAccess :D
First off, your original use of checkAccess was correct. Using Yii::app()->user->checkAccess() you are using the following definition:
http://www.yiiframework.com/doc/api/1.1/CWebUser#checkAccess-detail
Now, CWebUser's implementation of checkAccess calls CPHPAuthManager's implementation, which is where you encountered your problem with an illegal offset type.
http://www.yiiframework.com/doc/api/1.1/CPhpAuthManager#checkAccess-detail
An Illegal offset type means you are attempting to access an array element by specifying its key (also known as: offset) with a value that doesn't work as a key. This could be another array, an object, null, or possibly something else.
Your stack trace posted on the extensions page reveals that the following line gives the problem:
if(isset($this->_assignments[$userId][$itemName]))
So we have two possibilities for the illegal offset: $userId and $itemName.
Since $itemName is clearly a string, the problem must be with $userId.
(As a side note, the fact that your stack trace revealed surrounding code of this error also revealed that, at least for CPHPAuthManager, you are using a version of Yii that is prior to 1.1.11. Observe that lines 73 and 74 of https://github.com/yiisoft/yii/blob/1.1.11/framework/web/auth/CPhpAuthManager.php do not exist in your file's code.)
At this point I would have guessed that the problem is that the specified user is not logged in, and so Yii::app()->user->id is returning null. However, the new error you encountered when placing Yii::app()->user->id as the 2nd parameter of checkAccess reveals something else.
Since the 2nd parameter is in fact what should be the $params array that appears in your bizRule. Based on the error message, this means that Yii::app()->user->id is returning a mondoId type object.
I was unfamiliar with this type of object, so looked it up:
http://php.net/manual/en/class.mongoid.php
Long story short, you need to force Yii::app()->user->id to return the string value equivalent of this mondoId object. This likely set in your UserIdentity class in the components folder. To force it to be a string, simply place (string) to force a type conversion.
Example:
$this->_id = (string)$User->_id;
Your exact code will vary, based on what is in your UserIdentity class.
Then, restore your checkAccess to the signature you had before, and it should eliminate the Illegal offset error you encountered originally.
Note however that I have not used this extension, and while performing the following actions should fix this issue, it may cause new issues if the extension relies on the fact that Yii::app()->user->id is a mondoId object, and not a string.

Zend Form Validator : Element A or Element B

I have two fields in my Zend Form, and i want to apply the validation rule that ensures the user enters either one of the these two fields.
$companyname = new Zend_Form_Element_Text('companyname');
$companyname->setLabel('Company Name');
$companyname->setDecorators($decors);
$this->addElement($companyname);
$companyother = new Zend_Form_Element_Text('companyother');
$companyother->setLabel('Company Other');
$companyother->setDecorators($decors);
$this->addElement($companyother);
How can i add a validator that will look at both fields?
See the 'Note: Validation Context' on at this page. Zend_Form passes the context along to every Zend_Form_Element::isValid call as the second parameter. So simply write your own validator that analyzes the context.
EDIT:
Alright, I thought I'ld take a shot at this myself. It's not tested, nor is it a means to all ends, but it will give you a basic idea.
class My_Validator_OneFieldShouldBePresent extend Zend_Validator_Abstract
{
const NOT_PRESENT = 'notPresent';
protected $_messageTemplates = array(
self::NOT_PRESENT => 'Field %field% is not present'
);
protected $_messageVariables = array(
'field' => '_field'
);
protected $_field;
protected $_listOfFields;
public function __construct( array $listOfFields )
{
$this->_listOfFields = $listOfFields;
}
public function isValid( $value, $context = null )
{
if( !is_array( $context ) )
{
$this->_error( self::NOT_PRESENT );
return false;
}
foreach( $this->_listOfFields as $field )
{
if( isset( $context[ $field ] ) )
{
return true;
}
}
$this->_field = $field;
$this->_error( self::NOT_PRESENT );
return false;
}
}
Usage:
$oneOfTheseFieldsShouldBePresent = array( 'companyname', 'companyother' );
$companyname = new Zend_Form_Element_Text('companyname');
$companyname->setLabel('Company Name');
$companyname->setDecorators($decors);
$companyname->addValidator( new My_Validator_OneFieldShouldBePresent( $oneOfTheseFieldsShouldBePresent ) );
$this->addElement($companyname);
$companyother = new Zend_Form_Element_Text('companyother');
$companyother->setLabel('Company Other');
$companyother->setDecorators($decors);
$companyname->addValidator( new My_Validator_OneFieldShouldBePresent( $oneOfTheseFieldsShouldBePresent ) );
$this->addElement($companyother);
The solution provided by #fireeyedboy is handy but not working for this exact issue.
Zend_Validate_Abstract is using the context, which cannot be passed as variable to isValid(). This way when using the isValid() method (no matter if the original or overwritten one) the empty fields are not passed over and validated (unless you have setRequired(true) or setAllowEmpty(false), which we don't want). So in the case when you leave both two fields (companyname and companyother) empty, no action will take place. The only solution I am aware of is extending the Zend_Validate class to allow empty fields being validated.
Please let me know if you know better solution as I am struggling with similar problem too.
I haven't come across such a solution, but it's perfectly valid so +1.
I would extend Your_Form::isValid() to include a manual check for the values of those two elements.
If all fields pass their own individual validators, this validation probably belongs on the form as-a-whole and such it could be placed on the validation of the form instead of the fields. Do you agree with this line of thinking?
I agree with #chelmertz that a feature like this does not exists.
What I don't agree is extending Your_Form::isValid(). Instead, I'd write a custom Validator that accepts the values of both form elements that have to have a value. This way I could reuse it on arbitrary form elements. This is somewhat similar to the Identical Validator.

How do you Unit Test a Zend_Form that includes the CSRF form element?

I'm using the CSRF hidden hash element with Zend_Form and trying to Unit Test the login but don't know how to write a Unit Test to include that element. Looked in the docs and read as many tutorials as I could find. I even delicioused them all, but no one mentions this.
Csrf value is generated each time form is rendered. Hidden element of the form gets prefilled with that value. This value also gets stored in session.
After submitting form, validation checks if value posted from the form is stored in session, if not then validation fails. It is essential, that form must be rendered during the test (so it can generate the hidden value and store it to session), then we can extract what is the hidden value out of rendered html, and later we can add hidden hash value into our request.
Consider this example:
function testAddPageStoreValidData()
{
// render the page with form
$this->dispatch('/form-page');
// fetch content of the page
$html = $this->getResponse()->getBody();
// parse page content, find the hash value prefilled to the hidden element
$dom = new Zend_Dom_Query($html);
$csrf = $dom->query('#csrf')->current()->getAttribute('value');
// reset tester for one more request
$this->resetRequest()
->resetResponse();
// now include $csrf value parsed from form, to the next request
$this->request->setMethod('POST')
->setPost(array('title'=>'MyNewTitle',
'body'=>'Body',
'csrf'=>$csrf));
$this->dispatch('/form-page');
// ...
}
The correct hash is stored in the session, and the Hash form element has a Zend_Session_Namespace instance which contains the namespace for the hash.
To unit test the element, you would replace the Zend_Session_Namespace instance in the element (with setSession) with one you create yourself which contains the correct hash (the hash is stored in key "hash")
For further examples you could probably look at the Zend Framework unit tests for the Zend_Form_Element_Hash class. I would assume they have had to deal with this as well.
I set an environment variable in my Apache vhost file, which tells the code which server it's running on:
development, staging, or production
The line for the vhost file is:
SetEnv SITE_ENV "dev"
Then I just make my forms react to the appropriate environment:
if($_SERVER['SITE_ENV']!='dev')
{
$form_element->addValidator($csrf_validator);
}
I use this same technique for lots of stuff. For example, if it IS dev, I redirect all outgoing email to me, etc.
I answered a more recent question similar to this one. I'm putting my answer here as well in case it helps anybody in the future.
I recently found a great way of testing forms with hash elements. This will use a mock object to stub away the hash element and you won't have to worry about it. You won't even have to do a session_start or anything this way. You won't have to 'prerender' the form either.
First create a 'stub' class like so
class My_Form_Element_HashStub extends Zend_Form_Element_Hash
{
public function __construct(){}
}
Then, add the following to the form somewhere.
class MyForm extends Zend_Form
{
protected $_hashElement;
public function setHashElement( Zend_Form_Hash_Element $hash )
{
$this->_hashElement = $hash;
return $this;
}
protected function _getHashElement( $name = 'hashElement' )
{
if( !isset( $this->_hashElement )
{
if( isset( $name ) )
{
$element = new Zend_Form_Element_Hash( $name,
array( 'id' => $name ) );
}
else
{
$element = new Zend_Form_Element_Hash( 'hashElement',
array( 'id' => 'hashElement' ) );
}
$this->setHashElement( $element );
return $this->_hashElement;
}
}
/**
* In your init method you can now add the hash element like below
*/
public function init()
{
//other code
$this->addElement( $this->_getHashElement( 'myotherhashelementname' );
//other code
}
}
The set method is there just for testing purposes really. You probably won't use it at all during real use but now in phpunit you can right the following.
class My_Form_LoginTest extends PHPUnit_Framework_TestCase
{
/**
*
* #var My_Form_Login
*/
protected $_form;
/**
*
* #var PHPUnit_Framework_MockObject_MockObject
*/
protected $_hash;
public function setUp()
{
parent::setUp();
$this->_hash = $this->getMock( 'My_Form_Element_HashStub' );
$this->_form = new My_Form_Login( array(
'action' => '/',
'hashElement' => $this->_hash
}
public function testTrue()
{
//The hash element will now always validate to true
$this->_hash
->expects( $this->any() )
->method( 'isValid' )
->will( $this->returnValue( true ) );
//OR if you need it to validate to false
$this->_hash
->expects( $this->any() )
->method( 'isValid' )
->will( $this->returnValue( true ) );
}
}
You HAVE to create your own stub. You can't just call the phpunit method getMockObject() because that will directly extend the hash element and the normal hash element does 'evil' stuff in its constructor.
With this method you don't even need to be connected to a database to test your forms! It took me a while to think of this.
If you want, you can push the setHashElement() method ( along with the variable and the get method ) into some FormAbstract base class.
REMEMBER, in phpunit you HAVE to pass the hash element during form construction. If you don't, your init() method will get called before your stub hash can be set with the set method and you'll end up using the regular hash element. You'll know you're using the regular hash element because you'll probably get some session error if you're NOT connected to a database.
Let me know if you find this helpful or if you use it.
Solution for ZF2 is creating your form in test, and getting value from your csrf form element:
$form = new \User\Form\SignupForm('create-user');
$data = [
'security' => $form->get('security')->getValue(),
'email' => 'test#test.com',
'password' => '123456',
'repeat-password' => '123456',
];
$this->dispatch('/signup', 'POST', $data);