cakephp: model validation based on another field - validation

I am trying to setup Model validation on one field that only need to be checked if another field equals a particular value.
My first field is query which is a dropdown with many values, one value is 'Other' if this is selected then I need the second field 'query_other' to not be empty.
I have this setup in my Item Model:
public $validate = array(
'query' => array(
'notempty' => array(
'rule' => array('notempty'),
'message' => 'THE QUERY IS REQUIRED',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'query_other' => array(
'notempty' => array(
'rule' => array('if_query_other', 'query'),
'message' => 'REASON IS REQUIRED',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
);
I then have this custom function which is being called above.
function if_query_other(&$data, $check, $query_field) {
if($this->data[$this->name][$query_field] == 'Other' && $check == NULL)
{
return false;
}
else
{
return true;
}
}
It's not working, I am currently getting this error: Parameter 1 to Item::if_query_other() expected to be a reference, value given
CakePHP Version 2.3.6
Thanks

The error message is pretty clear, parameter 1 is passed by value, not by reference as your method signature requires. There is nothing to be passed by reference to a custom validation method, so simply remove the &, respectively remove the first parameter from the signature altogether.
The first parameter passed to a custom validation method will always be the data of the field to validate (in key => value format), followed by possible parameters defined in the rule array, like for example your fieldname. So $check would have never been null unless you would have defined null in the rule array, ie 'rule' => array('if_query_other', null), consequently your third parameter would have never been the fieldname.
Long story short, you need to define two parameters only, the first will contain the data of the field to validate, the second one the additional value defined in the rule array.
Here's an example, it checks whether the field passed in $query_field exists and whether its value equals Other, if it does it returns whether the value of the current field is not empty (I'm assuming the built-in Validation::notEmpty() is sufficient enough for your "not empty" check needs). If the value of the $query_field field doesn't equal Other, then this rule will always validate successfully, ie the value then isn't required to be not empty.
...
App::uses('Hash', 'Utility');
App::uses('Validation', 'Utility');
class Item extends AppModel
{
...
public function if_query_other($check, $query_field)
{
if(Hash::get($this->data[$this->alias], $query_field) === 'Other')
{
return Validation::notEmpty(current($check));
}
return true;
}
}

Related

Handling CodeIgniter form validation (rule keys and data types)

Okay, so I've been searching for a while this question, but couldn't find an answer (or at least some direct one) that explains this to me.
I've been using CodeIgniter 3.x Form Validation library, so I have some data like this:
// Just example data
$input_data = [
'id' => 1,
'logged_in' => TRUE,
'username' => 'alejandroivan'
];
Then, when I want to validate it, I use:
$this->form_validation->set_data($input_data);
$this->form_validation->set_rules([
[
'field' => 'id',
'label' => 'The ID to work on',
'rules' => 'required|min_length[1]|is_natural_no_zero'
],
[
'field' => 'username',
'label' => 'The username',
'rules' => 'required|min_length[1]|alpha_numeric|strtolower'
],
[
'field' => 'logged_in',
'label' => 'The login status of the user',
'rules' => 'required|in_list[0,1]'
]
]);
if ( $this->form_validation->run() === FALSE ) { /* failed */ }
So I have some questions here:
Is the label key really necessary? I'm not using the Form Validation auto-generated error messages in any way, I just want to know if the data passed validation or not. Will something else fail if I just omit it? As this will be a JSON API, I don't really want to print the description of the field, just a static error that I have already defined.
In the username field of my example, will the required rule check length? In other words, is min_length optional in this case? The same question for alpha_numeric... is the empty string considered alpha numeric?
In the logged_in field (which is boolean), how do I check for TRUE or FALSE? Would in_list[0,1] be sufficient? Should I include required too? Is there something like is_boolean?
Thank you in advance.
The "label" key is necessary, but it can be empty.
The "required" rule does not check length, nor does the "alpha_numeric". It checks that a value is present, it does not check the length of said value. For that, there is min_length[] and max_length[].
If you're only passing a 0 or 1, then this is probably the easiest and shortest route.

Create form in Controller using Sonata field type

In Symfony admin, I have a form, where the second field type depends on the ChoiceField value selected. The second field can be of Symfony Url field type or sonata provided sonata_type_model_list field type.
I have created an ajax request to My Bundle Controller to return the form, which contains the needed field.
> /src/MyBundle/Controller/MyController.php
namespace MyBundle\Controller
use Sonata\AdminBundle\Controller\CRUDController;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Sonata\AdminBundle\Form\FormMapper;
class MyController extends CRUDController
{
public function getFieldAction()
{
//getting the value of choice field
$type = $this->get('request')->get('type');
//sonata.admin.reference is a service name of ReferenceBundle admin class
$fieldDescription = $this->admin->getModelManager()
->getNewFieldDescriptionInstance($this->admin->getClass(), 'reference');
$fieldDescription->setAssociationAdmin($this->container->get('sonata.admin.reference'));
$fieldDescription->setAdmin($this->admin);
$fieldDescription->setAssociationMapping(array(
'fieldName' => 'reference',
'type' => ClassMetadataInfo::ONE_TO_MANY,
));
// Getting form mapper in controller:
$contractor = $this->container->get('sonata.admin.builder.orm_form');
$mapper = new FormMapper($contractor, $this->admin->getFormBuilder(), $this->admin);
$form_mapper = $mapper->add('reference', 'sonata_type_model_list', array(
'translation_domain' => 'ReferenceBundle',
'sonata_field_description' => $fieldDescription,
'class' => $this->container->get('sonata.admin.reference')->getClass(),
'model_manager' => $this->container->get('sonata.admin.reference')->getModelManager(),
'label' => 'Reference',
'required' => false,
));
//#ToDo build $form from $form_mapper
return $this->render('MyBundle:Form:field.view.html.twig', array(
'form' => $form->createView(),
));
}
}
I cannot find any method in Sonata\AdminBundle\Form\FormMapper class to build a form (it seems to be possible with create() method, but it only works with common Symfony field types, not Sonata form field types, which are commonly generated in Block or Admin classes).
Is it possible to use Sonata\AdminBundle\Form\FormMapper in the Controller to build a form?
Or is there another way I can build a form with Sonata form field types in the Controller?
You should not use a controller, rather a Sonata Admin Service.
Sonata provides you with the 'sonata_type_choice_field_mask' type which allows you to change the fields displayed on the form dynamically depending on the value of this 'sonata_type_choice_field_mask' input.
Here is the doc where you can find everything about sonata types and the choice field mask.
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('reference', 'sonata_type_choice_field_mask', array(
'choices' => array(
//The list of available 'Reference' here
'choice1',
'choice2'
),
'map' => array(
//What you want to display depending of the selected option
'choice1' => array(
// List of the fields displayed if choice 1 is selected
'field1', 'field3'
),
'choice2' => array(
// List of the fields displayed if choice 2 is selected
'field2', 'field3'
)
),
'placeholder' => 'Choose an option',
'required' => false
))
->add('field1', 'sonata_type_model_list', array(/* Options goes here */))
->add('field2', 'url', array(/* Options goes here */))
->add('field3')
;
}
It looks like you have access to the admin which directly handles building the form with Sonata\AdminBundle\Admin\AbstractAdmin::getForm.
$form = $this->admin->getForm();

CakePHP Validate a specific rule only when a couple required fields aren't empty

I wrote a custom rule method for validating whether a record exists in the DB before adding a new record. I put the method in a behavior so I could share it with other models, but I've run into a chicken and egg situation.
In order to know whether a category has a specific group name already I need to have the category id, and the group name. So I pass those keys through using my custom rule (category_id and name). But, this won't work since if I don't choose a category_id by mistake then the query will occur on just the name, so I patched this with a couple lines, but need to return true if this is the case and bank on the category_id validation being invalid.
Is there a better way to implement this kind of validation? Is this not as bad as I think? Or just don't bother and in my controller drop hasAny() under my call to validates() if it passes.
MODEL:
public $validate = [
'category_id' => [
'rule' => 'notEmpty',
'message' => 'Category is required.'
],
'name' => [
'notEmpty' => [
'rule' => 'notEmpty',
'message' => 'Team is required.'
],
'recordExists' => [
'rule' => [ 'recordExists', [ 'category_id', 'name' ] ],
'message' => 'Group already exists.'
]
]
];
// BEHAVIOR:
public function recordExists( Model $Model, $conditions, $requireKeys )
{
// Overrite conditions to
$conditions = $Model->data[ $Model->name ];
// Trim all array elements and filter out any empty indexes
$conditions = array_map( 'trim', $conditions );
$conditions = array_filter( $conditions );
// Get the remaining non-empty keys
$conditionKeys = array_keys( $conditions );
// Only query for record if all required keys are in conditions
if (empty( array_diff( $requireKeys, $conditionKeys ) )) {
return !$Model->hasAny( $conditions );
}
// NOTE: seems wrong to return true based on the assumption the category_id validation has probably failed
return true;
}
Use the beforeValidate() callback of the model to check if the fields are present and if they're empty. If they're empty just unset() the recordExists validation rule in your models validation property. Copy them to a temporary variable or property in the case you want to set them back after your current operation.
And use $Model->alias, name will break if the model is used through an association that has a different name.
$conditions = $Model->data[ $Model->name ];

Filtering a text-type column with MySQL-computed values in Magento Admin Grid

Say one column in grid has computed values:
setCollection():
'refunded' => new Zend_Db_Expr("IF(qty_refunded > 0, 'Yes', 'No')"),
_prepareColumns():
$this->addColumnAfter('refunded', array(
'header' => Mage::helper('helper')->__('Refunded'),
'index' => 'refunded',
'type' => 'text',
), 'qty');
What and how must one change in order to have columns with "yes" values, in case admin types "yes" then filters?
Adminhtml grid columns have a filter property which specifies a block class. For boolean yes/no fields that would usually be adminhtml/widget_grid_column_filter_select.
It would be used automatically if your field type would be 'options'.
Try this in _prepareCollection():
'refunded' => new Zend_Db_Expr("IF(qty_refunded > 0, 1, 0)"),
And in _prepareColumns() use:
$this->addColumnAfter('refunded', array(
'header' => Mage::helper('helper')->__('Refunded'),
'index' => 'refunded',
'type' => 'options',
'options' => array(0 => $this->__('No'), 1 => $this->__('Yes'))
), 'qty');
This should still render your values as "Yes" and "No" in the Column, and you would get the select with the appropriate options as a filter dropdown.
This alone won't be enough since the column with the computed value can't be referenced directly in the WHERE clause by MySQL. Magento provides two options to work around that.
Column filter blocks have a method getCondition() which return a condition that will be used to filter the collection. See Mage_Adminhtml_Block_Widget_Grid_Column_Filter_Abstract::getCondition() for an example.
So if you need to customize the SQL used to execute the filter, create your own column filter block extending Mage_Adminhtml_Block_Widget_Grid_Column_Filter_Select and adjust the returned condition as needed, i.e. use the same computed value to match against.
Your custom filter can be specified for the column like this in the addColumn() definition:
'type' => 'options',
'options' => array(0 => $this->__('No'), 1 => $this->__('Yes')),
'filter' => 'your_module/adminhtml_widget_grid_column_filter_custom',
If you prefere to work outside of the limitations of Magento's ORM filter syntax, you can modify the collections select directly by using a filter callback:
'type' => 'options',
'options' => array(0 => $this->__('No'), 1 => $this->__('Yes')),
'filter_condition_callback' => array($this, '_applyMyFilter'),
The callback receives the collection and the column as arguments. Here is a simple example for that method:
protected function _applyMyFilter(Varien_Data_Collection_Db $collection, Mage_Adminhtml_Block_Widget_Grid_Column $column)
{
$select = $collection->getSelect();
$field = $column->getIndex();
$value = $column->getFilter()->getValue();
$select->having("$field=?, $value);
}
Needless to say that both approaches (filtering against the computed value) is very inefficient in MySQL. But maybe that's no a problem for you in this case.
I'll post a working example, but I'll choose Vinai's answer for being so detailed.
In Grid.php:
protected function _addColumnFilterToCollection($column)
{
if ($column->getId() == 'refunded' && $column->getFilter()->getValue()) {
$val = $column->getFilter()->getValue();
$comparison = ($val === "No") ? 'lteq' : 'gt'; // lteg: <=; gt: >
$this->getCollection()->addFieldToFilter('ois.qty_refunded', array($comparison => 0));
} else {
parent::_addColumnFilterToCollection($column);
}
return $this;
}

Validation rule for a composite unique index (non-primary)

I am sure I am not the first who has composite unique keys in tables and who wants to validate them. I do not want to invent the bicycle so I ask here first. I have several tables that have 'id' columns as primary keys and two other columns as unique composite keys. It would be nice to have a validation rule to check that the submitted entry is unique and display a validation error if it is not. In Cakephp it could be done by a custom validation rule. I am pretty sure somebody has created such method already.
Ideally it would be a method in app_model.php that could be used by different models.
I am using that function:
function checkUnique($data, $fields) {
if (!is_array($fields)) {
$fields = array($fields);
}
foreach($fields as $key) {
$tmp[$key] = $this->data[$this->name][$key];
}
if (isset($this->data[$this->name][$this->primaryKey]) && $this->data[$this->name][$this->primaryKey] > 0) {
$tmp[$this->primaryKey." !="] = $this->data[$this->name][$this->primaryKey];
}
//return false;
return $this->isUnique($tmp, false);
}
basically the usage is:
'field1' => array(
'checkUnique' => array(
'rule' => array('checkUnique', array('field1', 'field2')),
'message' => 'This field need to be non-empty and the row need to be unique'
),
),
'field2' => array(
'checkUnique' => array(
'rule' => array('checkUnique', array('field1', 'field2')),
'message' => 'This field need to be non-empty and the row need to be unique'
),
),
So basically this will show the warning under each of the fields saying that it's not unique.
I am using this a lot and it's working properly.
In the CakePHP/2.x versions released in the last few years, the isUnique rule optionally accepts several columns:
You can validate that a set of fields are unique by providing multiple
fields and set $or to false:
public $validate = array(
'email' => array(
'rule' => array('isUnique', array('email', 'username'), false),
'message' => 'This username & email combination has already been used.'
)
);
I'm not sure of the exact version when the feature was available but there's a bug fixed in core as late as October 2014 filed against 2.3 and 2.4 branches.
You could put it in app model, but my suggestion would just be to add it to the model directly by placing the rule with it's $validate property.
Check out the built in isUnique rule.

Resources