Zend_Validate_Between on Another Value - validation

What is the best way to validate a number range based on the value of another form element? If the user selects "percentage" as a discount type, the discount amount should be between 0 and 100, and not 140! The problem seems to be passing in another form element value.
Also, I've viewed other resources, one dealing with a similar topic, but perhaps not all the way relevant.
How to validate a field of Zend_Form based on the value of another field?
Form.php
$isValid = new Application_Model_Validate();
$discount = $this->createElement('text', 'discount')
->setLabel('Discount Amount')
->setDescription("Enter an amount in the format \"200.00\" ")
->setRequired(true)
->setDecorators(array('Description', 'ViewHelper', 'Errors',
array('HTMLTag', array('tag' => 'dd')),
array('Label', array('tag' => 'dt'))));
$discount->addValidators(array(array('Float')), $isValid->isValid(new Zend_Validate_Between(array('min' => '0', 'max' => '100')), $discountType));
$this->addElement($discount);
Application_Model_Validate.php
Require_once 'Zend/Validate/Abstract.php';
class Application_Model_Validate extends Zend_Validate_Abstract
{
/*
* Validation failure message key
*/
const INVALID_PERCENTAGE = 'InvalidPercentage';
/*
* Validation failure message template definitions
*/
protected $_messageTemplates = array(
self::INVALID_PERCENTAGE => 'Please enter a percentage greater than 0 and up to 100.'
);
protected $_percentageOption;
protected $_percentageValue;
/*
* Defined by Zend_Validate_Interface
* Validate the percentage parameters
*/
public function isValid($value, $context = null)
{
$this->_setValue($value);
/*
* If context key is valid, return true
*/
if(is_array($context))
{
if (isset($context['percentage']) && ($value))
{
return true;
}
}
$this->_error(self::INVALID_PERCENTAGE);
return false;
}
If you need anymore information, just say.

I modified your code a bit and added a drop down box:
Form:
$this->addElement('select', 'discounttype');
$this->getElement('discounttype')
->addMultiOptions(
array('percentage' => 'percentage', 'other' => 'other')
);
$discount = $this->createElement('text', 'discount')
->setLabel('Discount Amount')
->setRequired(true);
$discount->addValidators(
array('Float', new Application_Model_Validate(0, 140))
);
$this->addElement($discount);
Validator:
<?php
class Application_Model_Validate extends Zend_Validate_Between
{
public function isValid($value, $context = null)
{
$this->_setValue($value);
if ($context['discounttype'] == 'percentage') {
$this->setMax(100);
}
return parent::isValid($value, $context);
}
}
Now, if you validate the form in your controller using $form->isValid($this->getRequest()->getParams()), it will take an input between 0 and 100 if in the drop down box 'percentage' is selected and an input between 0 and 140 otherwise.

An alternative solution:
//controller validation
$discountValidate = new Application_Model_Validate();
$discountValidate->_checkDiscount($data['discountType'], $data['discount']);
if ($discountValidate->isValid())
{
//do
}
else
{
$element = $form->getElement('discount');
$element->addError($discountValidate->getError());
}

Related

Laravel - How to update Input Array without deleting Sales Detail

In my Laravel-8 project, I have this controller for Input Field Array Update.
Controller:
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
SaleDetail::where('sale_id', $sale->id)->delete();
foreach ($data['invoiceItems'] as $item) {
$details = [
'sale_id' => $sale->id,
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$saleDetail = new SaleDetail($details );
$saleDetail->save();
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
In the form, the user can add more Sales Detail or remove.
Some of the SaleDetail fields are being used somewhere else.
Is there a way to update the input field array without deleting the SaleDetail as shown in what I did here:
SaleDetail::where('sale_id', $sale->id)->delete();
Thanks
I've tried to restructure your code so that's easier to edit. I've left some comments. I can really recommend refactoring.guru. There you will find many ways to improve your code so that it is more extensible, maintainable and testable. If you have any questions, please feel free to ask.
class Sale extends Model
{
// Use a relationship instead of building your own query
public function details() {
return $this->hasMany(SaleDetail::class);
}
}
class SaleDetail extends Model
{
// Use a computed property instead of manually calculating total price
// You can access it with $saleDetail->totalPrice
public function getTotalPriceAttribute() {
return $this->price * $this->quantity;
}
}
class UpdateSaleRequest extends Request
{
public function authorize() {
return true;
}
protected function prepareForValidation() {
$this->merge([
// Create a Carbon instance by string
'date' => Carbon::make($this->date)
]);
}
public function rules() {
// Your validation rules
// Please also validate your invoice items!
// See https://laravel.com/docs/8.x/validation#validating-arrays
}
}
// We let Laravel solve the sale by dependency injection
// You have to rename the variable name in ihr web.php
public function update(UpdateSaleRequest $request, Sale $sale)
{
// At this point, all inputs are validated!
// See https://laravel.com/docs/8.x/validation#creating-form-requests
$sale->update($request->validated());
// Please ensure, that all properties have the same name
// In your current implementation you have price = cost, be consistent!
foreach($request->input('invoiceItems') as $invoiceItem) {
// How we can consider that a detail is already created?
// I assume that each item_id will only occur once, otherwise you'll
// place the id of each detail in your update form (e.g. in a hidden input)
$candidate = $sale->details()
->where('item_id', $properties['item_id'])
->first();
if($candidate) {
$candidate->update($properties);
} else {
$sale->details()->create($properties);
}
}
// A JWT-Exception should not be necessary, since your authentication
// will be handled by a middleware.
return response()->json($sale);
}
I have not tested the code, few adjustments may be needed.
Laravel has a method called updateOrCreate as follow
/**
* Create or update a record matching the attributes, and fill it with values.
*
* #param array $attributes
* #param array $values
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}
That means you could do some thing like
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
foreach ($data['invoiceItems'] as $item) {
$details = [
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$sale->saleDetail()->updateOrCreate([
'sale_id' => $sale->id
], $details);
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
I would encourage you to refactor and clean up your code.You can also read more about it here https://laravel.com/docs/8.x/eloquent#upserts

add Symfony Assert in a Callback

I, i have to add an Assert to an atribute when other atribute is equal than something. Like this:
/**
* #Assert\Callback(methods={"isChildMinor",)
*/
class PatientData
{
/**
* #Assert\Date()
*/
public $birthday;
public $role;
public function isChildMinor(ExecutionContext $context)
{
if ($this->role == 3 && check #assert\isMinor() to $birtday) {
=>add violation
}
}
so, i want check if the patient is minor (with assert or somethings else) if the role is equal than 3. How do this?
There are several ways to do, what you want.
1) You could make it right in the form. Like that:
use Symfony\Component\Validator\Constraints as Assert;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$yourEntity = $builder->getData();
//here you start the field, you want to validate
$fieldOptions = [
'label' => 'Field Name',
'required' => true,
];
if ($yourEntity->getYourProperty != 'bla-bla-bla') {
$fieldOptions[] = 'constraints' => [
new Assert\NotBlank([
'message' => 'This is unforgivable! Fill the field with "bla-bla-bla" right now!',
]),
],
}
$builder->add('myField', TextType::class, $fieldOptions);
2) Other way - is to make your custom validation callback in your Entity and play with direct asserts there. It's possible, I think.
3) But the optimal way, from my point of view - is to use several asserts with validation groups. You need to specify Assert\isMinor(groups={"myCustomGroup"}) on birthday field. And then, in your form:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'validation_groups' => function (FormInterface $form) {
$yourEntity = $form->getData();
if ($yourEntity->role !== 3) {
return ['Default', 'myCustomGroup'];
}
return ['Default'];
},
Hope this'll be helpful for you.

Symfony2 validate text input when containing something only

I want to validate a form which holds a dropdown menu and a text input field.
The user can choose a project from the dropdown menu. If he wants to create a new project he can use the text input field next to the dropdown menu.
Here is my upload type:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setAction('upload')
->setMethod('POST')
// project name dropdown menu
->add('projectname', 'choice' , array(
'label' => 'upload_project_label',
'choices' => $this->projects,
'attr' => array(
'class' => 'form-control some',
'required' => 'true'
)
))
// newprojectname text input
->add('newprojectname', 'text', array(
'label' => false,
'attr' => array(
'class' => 'form-control',
'required' => false,
'placeholder' => 'upload_newprojectname_placeholder'
)
)
)
...
And this is a snippet from my upload entity:
/**
* #ORM\Column(type="text")
*
* #var string $projectname
* #Assert\NotBlank()
*/
protected $projectname;
/**
* #ORM\Column(type="text")
*
* #var string $newprojectname
* #Assert\Length(
* min = 3,
* max = 7,
* minMessage = "min message",
* maxMessage = "max message"
* )
*/
protected $newprojectname;
My question is is there a possibility of a query to check if the field newproject is set (i.e. a string is entered)? And if so let the Assert annotation do its job.
This can be done a number of ways, all of which will likely satisfy your requirements.
Use a custom callback - this is the quickest and most straightforward
Use an expression validator - a lot of people have issues with embedding a meta-language within PHP which is perfectly valid but this is another quick-ish way of doing things
Use group sequences, specifically the group sequence provider functionality
Which one you choose is up to you but callbacks are a quick and easy starting point that you can build on if your validation constraints become more complex.
This is the code block for advised solution as custom callback validation.
I had to add another function in my upload entity which looks like this:
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Function holds custom validation for fields in import/upload form
* #Assert\Callback
* #param ExecutionContextInterface $context
*/
public function validate(ExecutionContextInterface $context)
{
// check new project name
$newProjectName = $this->getNewprojectname();
if(!empty($newProjectName)) {
// letters only
$pattern = '/[a-zA-Z]/';
if($this->isPatternWrong($newProjectName, $pattern)) {
$context
->buildViolation('Please use letters only.')
->atPath('newprojectname')
->addViolation();
}
// text max. 7 digits
$maxlength = 7;
if($this->isStringTooLong($newProjectName, $maxlength)) {
$context
->buildViolation('Max. length 7 digits.')
->atPath('newprojectname')
->addViolation();
}
}
}
private function isPatternWrong($string, $pattern)
{
$result = preg_match($pattern, $string);
if($result === 0) {
return true;
}
return false;
}
private function isStringTooLong($string, $length)
{
if(strlen($string) > $length) {
return true;
}
return false;
}

How to create a Symfony2.1 Validation Constraint to validate the number of tokens in a string

Sometimes happen that a collection of values is stored in a database as a unique string. This string is so made of all the values separated by a user-defined delimiter (e.g. "," or "_").
What would be nice in a Symfony2.1 application is to have a Validation Constraint that validates a string (e.g. provided by an input text form) by counting the number of tokens included in that string.
A possible example is when you store the tags in a string format, i.e. you receive a string from an input field like value1,value2,value10,value25. You see that 4 tokens are passed, but there is no form validator that does that control for you. So, one should use such a validator like:
/**
* #Assert\Token(
* delimiter=",",
* min = "1",
* max = "5",
* minMessage = "You must specify at least one token",
* maxMessage = "You cannot specify more than 5 tokens")
*/
$tags;
There is something similar when using the new in Symfony2.1 Count validator, but is doesn't work on strings, just on array of objects that implements Countable.
Who know how to implement that kind of "tokenized string" validator?
I solved my problem, I just want to share my solutions.
One possible solution is to use a Callback constraint. For instance, following the tag list example provided in the question:
/**
* #Assert\Callback(methods={"isTagStringValid"})
*/
class AFormModel{
protected $tags;
public function isTagStringValid(ExecutionContext $context){
$tagsExploded = explode(',', $this->tags);
if(count($tagsExploded)==0){
$context->addViolationAtSubPath('tags', 'Insert at least a tag', array(), null);
}
if(count($tagsExploded)==1 && $tagsExploded[0]==='')
$context->addViolationAtSubPath('tags', 'Insert at least a tag', array(), null);
}
else if(count($tagsExploded)>10){
$context->addViolationAtSubPath('tags', 'Max 10 values', array(), null);
}
}
}
A more elegant way is to define the "Token" validator. An example follows here:
namespace .....
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Token extends Constraint {
public $min;
public $max;
public $minMessage = '{{ min }} token(s) are expected';
public $maxMessage = '{{ max }} token(s) are expected';
public $invalidMessage = 'This value should be a string.';
public $delimiter = ',';
public function __construct($options = null){
parent::__construct($options);
if (null === $this->min && null === $this->max) {
throw new MissingOptionsException('Either option "min" or "max" must be given for constraint ' . __CLASS__, array('min', 'max'));
}
}
}
And the validator class is:
namespace ...
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class TokenValidator extends ConstraintValidator {
public function isValid($value, Constraint $constraint) {
if ($value === null) {
return;
}
if(!is_string($value)){
$this->context->addViolation($constraint->invalidMessage, array(
'{{ value }}' => $value,
));
return;
}
$tokensExploded = explode($constraint->delimiter, $value);
$tokens = count($tokensExploded);
if($tokens==1){
if($tokensExploded[0]==='')
$tokens = 0;
}
if (null !== $constraint->max && $tokens > $constraint->max) {
$this->context->addViolation($constraint->maxMessage, array(
'{{ value }}' => $value,
'{{ limit }}' => $constraint->max,
));
return;
}
if (null !== $constraint->min && $tokens < $constraint->min) {
$this->context->addViolation($constraint->minMessage, array(
'{{ value }}' => $value,
'{{ limit }}' => $constraint->min,
));
}
}
}
In this way you can import the user-defined validator and use it everywhere like I proposed in my question.

How to validate a date without day with sfForm?

I'm creating a payment form with symfony 1.4 , and my form has a date widget defined like this, so that the user can select the expiration date of his credit card:
new sfWidgetFormDate(array(
'format' => '%month%/%year%',
'years' => array_combine(range(date('Y'), date('Y') + 5), range(date('Y'), date('Y') + 5))
Notice the absence of %day% in the format like in most payment forms.
Now my problem is that sfValidatorDate requires the 'day' field not to be empty. To work around this, I created a custom validator using a callback, which works well:
public function validateExpirationDate($validator, $value)
{
$value['day'] = '15';
$dateValidator = new sfValidatorDate(array(
'date_format' => '#(?P<day>\d{2})(?P<month>\d{2})(?P<year>\d{2})#',
'required' => false,
'min' => strtotime('first day of this month')));
$dateValidator->clean($value);
return $value;
}
I feel there might be a simpler way to achieve this. What do you think? Have you already solved this problem in a cleaner way?
How do you store the date? If you just store month and year as integers or strings, then you can just make 2 choice widgets. But if you store it as datetime (timestamp), then you need a valid date anyway. This means that you need to automatically assign values to 'day' (usually first or last day of the month).
class YourForm extends BaseYourForm
{
public function configure()
{
$this->widgetSchema['date'] = new sfWidgetFormDate(array(
'format' => '%month%/%year%'
));
$this->validatorSchema['date'] = new myValidatorDate(array(
'day_default' => 1
));
}
}
class myValidatorDate extends sfValidatorDate
{
protected function configure($options = array(), $messages = array())
{
$this->addOption('day_default', 1);
parent::configure($options, $messages);
}
protected function doClean($value)
{
if (!isset($value['day']))
{
$value['day'] = $this->getOption('day_default');
}
return parent::doClean($value);
}
}
There's no need to use a custom validation class: you can simply override the tainted values passed to your bind() method:
<?php
// in your form class
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
$taintedValues['date']['day'] = 1;
return parent::bind($taintedValues, $taintedFiles);
}
I used simplest way, for validate credit card expiration day:
$post_data = $request->getParameter('my_form');
$post_data['card_exp']['day'] = 1; //sets the first day of the month
$this->form->bind($post_data);
Hope this helps somebody.
I solve this first in the form class
$year = range(date('Y'), date('Y') - 50);
$this->widgetSchema['date'] = new sfWidgetFormDate(array(
'format' => '%year%',
'years' => array_combine($year, $year),
'can_be_empty' => false
));
Next...
public function bind(array $taintedValues = null){
$taintedValues['date']['day'] = '01';
$taintedValues['date']['month'] = '01';
parent::bind($taintedValues);
}
The field in the database is date type DATE.

Resources