I have a drop down list where users can select their timezone on a form. On submit, I'm looking for the best way to validate the timezone input field using Yii rules.
I'm able to get an array of timezones in PHP using the following:
DateTimeZone::listIdentifiers(DateTimeZone::ALL)
The values are like this (an indexed key with a timezone value):
Array([0]=>'Pacific/Midway',[1]=>'Pacific/Niue',...);
My select box looks like this:
<select name="timezone" id="timezone">
<option value="Pacific/Midway">Pacific/Midway (GMT-11:00)</option>
<option value="Pacific/Niue">Pacific/Niue (GMT-11:00)</option>
...
</select>
I tried the following:
public function rules() {
return array(
array('timezone','in','range'=>DateTimeZone::listIdentifiers(DateTimeZone::ALL)),
array(timezone','required'),
);
} // rules()
But it never validates, I think because the the range is a multidimensional array and in order for range to work it needs to be just an array of values like array('value1','value2'); Is that true?
Is there a better approach?
Best way is to create custom validator class, some example (untested, tune/fix to your needs):
class CountryValidator extends CValidator
{
/**
* Validates the attribute of the object.
* If there is any error, the error message is added to the object.
* #param CModel $object the object being validated
* #param string $attribute the attribute being validated
*/
protected function validateAttribute($object,$attribute)
{
$ids = DateTimeZone::listIdentifiers(DateTimeZone::ALL);
if(!in_array($object->$attribute, $ids))
{
$this->addError($object,$attribute,'Wrong time zone selected!');
}
}
}
Then use it in your model:
/**
* #return array validation rules for model attributes.
*/
public function rules()
{
return array(
array('timezone', 'alias.to.CountryValidator'),
);
}
This method has very important advantage: Like other validator it is reusable, and does not mess your model code.
Related
I have one quite simple question, Imagine I have Orders model and now I am writing something like that :
Order::where('status', 1)->with('orderer')->get();
Ok. It's simple and returns something like that:
{
id: 1,
price: 200,
status: 1,
income: 21,
orderer_id: 4,
orderer: {
//some orderer fields
}
}
now I don't want to get the whole object, I want to remove income, orderer_id and status properties from data. if I write something like that : get(["id", "price"]) I end up without orderer object (get(["id", "price", "orderer"]) doesn't work too), I couldn't make it work even using select(), so what is the solution? Also I don't want to hide it from everyone, for example admin should know income but user shouldn't, so $hidden field will not work.
You can add select() but make sure select does not take array but comma separated arguments :
$orders = Order::where('status', 1)->with('orderer');
if($user->role == 'admin'){
$orders->select('id','income','status','price');
}
else{
$orders->select('id','status','price');
}
$orders = $orders->get();
Above will first check the current logged in user's role and accordingly will select the columns required.
https://scotch.io/bar-talk/hiding-fields-when-querying-laravel-eloquent-models
In your Order Eloquent model:
protected $hidden = array('hide_this_field', 'and_that_field');
Edit: You want to filter based on role like Admin or User, next time please write that down in your question as well. Well a solution for that is to capture the DB query result, and walk that array, then unset properties of the model if the user is not an admin.
Edit2: I also see a discussion here which might help. Some user suggested using middle ware:
https://laracasts.com/discuss/channels/laravel/hide-eloquent-fields-based-on-user-role-or-any-model
If you are looking for a built in Laravel way to handle this, you could use API Resources: https://laravel.com/docs/5.7/eloquent-resources
php atrisan make:resource OrderResource
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
$current_role = $request->user()->role; //or however you determine admin etc
$out = [
'id' => $this->id,
'price' => $this->price,
'orderer'=> $this->orderer,
];
if($current_role == 'admin'){
$out['income'] = $this->income;
$out['status'] = $this->status;
}
return $out;
}
}
In your Controller action
return OrderResource::collection(Order::where('status', 1)->with('orderer')->get());
If you want something a little more robust, consider https://github.com/spatie/laravel-fractal
Let me describe my scenario. Here's a form similar to mine:
<select name="colors">
<option value="1">Dark red</option>
<option value="2">Light red</option>
<option value="3">Dark green</option>
<option value="4">Light green</option>
</select>
<input type="text" name="textbox-field">
<input type="checkbox" name="checkbox-field" value="1">
I'd like to validate textbox-field OR checkbox-field based on which value is selected on colors. The problem is that textbox-field and checkbox-field are mutually exclusive, which means that only one of them has to be filled when the specified colors values have been selected.
If I had one field I could just use the built-in required_if rule like this:
'textbox-field' => "string|required_if:colors,3,4"
However, what I want to achieve is somethink like this but with an OR operator between the last two rules:
'textbox-field' => "string|required_if:colors,3,4|required_without:checkbox-field"
'checkbox-field' => "numeric|required_if:colors,3,4|required_without:textbox-field"
Is it possible to create a custom validation rule which will combine required_if and required_without using the OR logical operator?
I was able to figure this out, by implementing the following custom validator:
/**
* Class CustomValidator
* Extends the Illuminate\Validation\Validator class instead of using Validator::extend,
* it requires access to protected properties of the Validator parent class
*
* Note: This needs to be registered in app/start/global.php
*/
class CustomValidator extends Illuminate\Validation\Validator
{
/**
* The validation rules that imply the field is required.
*
* #var array
*/
protected $implicitRules = array(
'Required',
'RequiredWith',
'RequiredWithAll',
'RequiredWithout',
'RequiredWithoutAll',
'RequiredIf',
'Accepted',
'RequiredIfValueSelectedOrWithoutField'
);
/**
* Validate mutually exclusive fields when specific options have been selected in select input
* This uses the built-in rules and apply them using the OR logical operator
*
* #param $attribute
* #param $value
* #param $parameters
*
* #return bool
*/
public function validateRequiredIfValueSelectedOrWithoutField($attribute, $value, $parameters) {
// use the required_if bult-in rule to check if the required options are selected in select input
$valueSelectedParams = array_merge(['my_select_input'],Model::getArrayWithTheSpecifiedOptions());
$isValidForSelect = $this->validateRequiredIf($attribute, $value, $valueSelectedParams);
// use the required_without to check if the mutually exclusive field in parameters[0] has a value
$isValidForMutualExclusiveField = $this->validateRequiredWithout($attribute, $value, [$parameters[0]]);
// returns the result by applying OR between the required_if and the required_without rules
return ($isValidForSelect || $isValidForMutualExclusiveField);
}
/**
* Replace the :other placeholder with the select input localized literal to display correctly in error messages
*
* #param $message
*
* #return mixed
*/
protected function replaceDangerousEkaOrField($message)
{
return str_replace(':other', Lang::get('model.my_select_input'), $message);
}
I placed this in app/helpers/CustomValidator.php and registered it at the end of my app/start/global.php:
/*
|--------------------------------------------------------------------------
| Register Custom Validator extending Illuminate\Validation\Validator
|--------------------------------------------------------------------------
*/
Validator::resolver(function($translator, $data, $rules, $messages)
{
return new CustomValidator($translator, $data, $rules, $messages);
});
It also required to run composer dump-autoload to add the new class to classloader.
I hope this to help anyone in a similar position in the future. Cheers!
I have two classes with a bidirectional relationship: Player and Team Each player must have exactly one team and each team can have many players.
When I use the default form field (select) and I submit 0 (by manually editing the HTML) the form error shows correctly. However if I use a custom form field type and submit 0 there's no form error but an exception:
Catchable Fatal Error: Argument 1 passed to
...\Entity\Player::setTeam() must be an instance of ...\Entity\Team,
null given, called in
...\vendor\symfony\symfony\src\Symfony\Component\PropertyAccess\PropertyAccessor.php
on line 360 and defined in ...\Entity\Player.php line 58
How do I show a normal form error instead of this exception?
My classes are straighforward (only the relevant parts posted):
Team.php
class Team {
/**
* #ORM\OneToMany(targetEntity="...\Entity\Player", mappedBy="team")
*/
protected $players;
}
Player.php
class Player {
/**
* #var Team
*
* #ORM\ManyToOne(targetEntity="...\Entity\Team", inversedBy="players")
* #ORM\JoinColumn(name="team_id", referencedColumnName="id", nullable=false)
* #Assert\Valid
*/
protected $team;
/**
* Set team
*
* #param Team $team
* #return Player
*/
public function setTeam(Team $team) {
$this->team = $team;
return $this;
}
/**
* Get team
*
* #return Team
*/
public function getTeam() {
return $this->team;
}
}
The reverseTransform function of my DataTransformer looks like this:
public function reverseTransform($value)
{
if(!$value)
{
return $this->multiple ? array() : 0;
}
//... get entity
}
If you have created a custom form field with a custom data transformer, it is your responsibility to validate client datas. To get the generic message (This value is not valid), you need to throw a TransformationFailedException in your data transformer. Then, everything will be handled by the form component.
EDIT: By default majority of the data transformers in the Symfony core, converts a blank value to null. The responsibility of the data transformer is to convert client data to model data and eventually to detect major error like non-acceptable value for a choice list or missing data in case of datetime field, etc. It is not to validate the data which is the resposibility of the validator component. Then, it lets the validator component make the final validation which is often more complex than a simple data transformer. IMHO, you're currently mixing data transfomer and validation concern.
EDIT: Additionally, you need to allow in your domain object what the data transformer return itself (here, the null value).
In my cake PHP application, I have a edit form where "email" field is readonly that means user can not update it.
NOw if I think according to security point of view, user can update the field by 'firebug' or some other browser plugins.
I am using $this->User->save($this->data) to save the updated data. By this function Email can be also be updated.
Do we have any way in cake php so that I can prevent this field to be update, like by passing here a argument or something like this?
You can simply remove the email field from $this->data:
unset($this->data['User']['email']);
$this->User->save($this->data);
You could do something like:
$dontUpdateField = array('email');
$this->Model->save(
$this->data,
true,
array_diff(array_keys($this->Model->schema()),$dontUpdateField)
);
If security is a concern, simply reject any data that has unexpected values. In cake you could do this, but it can be adapted for any framework/cms
/**
* Checks input array against array of expected values.
*
* Checks single dimension input array against array of expected values.
* For best results put this is in app_controller.
*
* #param array $data - 1 dimensional array of values received from untrusted source
* #param array $expected - list of expected fields
* #return boolean - true if all fields are expected, false if any field is unexpected.
*/
protected function _checkInput($data,$expected){
foreach(array_keys($data) as $key){
if (!in_array($key,$expected)){
return;
}
}
return true;
}
/**
* edit method.
*
* put this in <Model>_controller
* #param string $id
* #return void
* #todo create errors controller to handle incorrect requests
* #todo configure htaccess and Config/routes.php to redirect errors to errors controller
* #todo setup log functionality to record hack attempts
* #todo populate $expected with fields relevant to current model
*/
function edit($id=null){
$expected = ('expectedVal1', 'expectedVal2');
$this->Model->id = $id;
if (!$this->Model->exists()) {
throw new NotFoundException(__('Invalid model'));
}
if ($this->request->is('post')) {
if (!$this->_checkData($this->request->data['Model'], $expected)) {
//log the ip address and time
//redirect to somewhere safe
$this->redirect(array('controller'=>'errors','action'=>'view', 405);
}
if ($this->Model->save($this->request->data)) {
//do post save routines
//redirect as necessary
}
else {
$this->Session->setFlash(__('The model could not be saved. Please, try again.'));
}
}
$this->set('model',$this->Model->read($expected,$id));
}
You can use the security component and make the email hidden. While using this component, hidden fields cant be changed or cake will blackhole the form.
http://book.cakephp.org/1.3/en/view/1296/Security-Component
If your application is public it is strongly recommended that you use security, otherwise it is kinda trivial to inject data in your models by submitting extra fields on the form and when you do $this->Model->save($this->data)) the extra fields are saved, unless you do the extra work of validating every field of $this->data;
I would like to filter some fields in my form with strtolower() function. Unfortunately I can't find any example of doing that.
How can I write such filter, that will lowercase the input, check the database if element exists and then decide wheter to add the record or not?
1) new project custom validator (we will use it like value filter here):
/lib/validator/MyProjectStringLowerCase.class.php
<?php
class MyProjectStringLowerCase extends sfValidatorBase
{
/**
* #see sfValidatorBase
*/
protected function doClean($value)
{
return strtolower($value);
}
}
2) bound it to field:
$this->setWidget('my_field_name', new sfWidgetFormInputText());
$this->validatorSchema['my_field_name'] = new MyProjectStringLowerCase();
If you have some validator on that field already, you can merge them into combined validators this way:
$this->validatorSchema['my_field_name'] = new sfValidatorAnd(array(
$this->validatorSchema['my_field_name'], // the original field validator
new MyProjectStringLowerCase(),
));
The combined validators order influent how value will flow trough them, so if you want to have value filtrated in second validation, set MyProjectStringLowerCase as the first one.
There are 2 differences between this approach and using post processing (like doSave() for instance):
the value here will be filtered after each send (and will show
filtered in displaying of form errors)
You can reuse it very cleanly and easily in other fields or forms in
your project
In your Form, you can override the doSave() method to do any manual interventions that you need to do that aren't completed by the form validation methods.
For example:
public function doSave($con = null) {
$employee = $this->getObject();
$values = $this->getValues();
// do your filter
$this->values['name'] = strtolower($values['name']);
parent::doSave($con);
}