Yii model rules: only allow values specified in itemAlias - php

As a longtime procedural programmer, I finally made the switch to OOP/MVC in combination with Yii. I don't regret it at all, but I have a question which may be obvious.
In my model, generated by GII, I define rules and aliasses replacing values. Now I want the 'currency' value to only allow input specified in the aliasses. Ofcourse I can do something like is_number, greater that 0 and smaller than 4, but in that case I will have to update my code all the time when a new currency is added. Is there an easier way to do the input validation based on the defined values?
<?PHP
class Affiliateprograms extends CActiveRecord
{
//define rules
public function rules()
{
return array(array('currency', 'required'));
}
//set aliases
public static function itemAlias($type,$code=NULL)
{
$_items = array('currency' => array('1' => 'US Dollar','2' => 'Euro','3' => 'Yen'));
if (isset($code))
return isset($_items[$type][$code]) ? $_items[$type][$code] : false;
else
return isset($_items[$type]) ? $_items[$type] : false;
}
?>

You can use the builtin in validator (actually a CRangeValidator).
array('currency','in','range'=>array_keys(self::itemAlias('???')));
You need to insert the right $type of course.
Side note: Please reconsider your indenation style - it's pretty unusual and makes your code hard to read ;)

Related

How can I add function name as route in Laravel 5.7?

I have a controller which returns enums for respective fields. e.g.
// Expected route - /api/getFamilyTypes - only GET method is allowed
public function getFamilyTypes()
{
return [
'Nuclear Family',
'Joint Family'
];
}
I've around 20 functions like this. How can I add this without manually adding an entry per function in routes file?
Thanks in advance.
In your routes file, add something like this,
Route::get('/something/{func}', 'SomeController#functionRoute');
Where something is whatever path you're wanting to use and SomeController is the controller with the 20 functions you're using and functionRoute is the action that we're about to make.
Then in your controller, make a function like this,
public function functionRoute($func)
{
return $this->$func();
}
This will make it so that whenever someone browses to /something/* on your website, it'll execute the function name at the end. So if you navigate to /something/getFamilyTypes it'll run your getFamilyTypes function.
This isn't particularly secure. If you do this, the user will be able to run any of the controller's methods. You could set up a blacklist like this.
public function functionRoute($func)
{
$blacklist = [
'secret',
'stuff',
];
return in_array($func, $blacklist) ? redirect('/') : $this->$func();
}
Or you could set up a whitelist like this,
public function functionRoute($func)
{
$whitelist = [
'getFamilyTypes',
'otherUserFriendlyStuff',
];
return in_array($func, $whitelist) ? $this->$func() : redirect('/');
}
If the responses are always from hard-coded arrays (as opposed to being from a database) then one way might be to have a variable in your route:
Route::get('/api/enum/{field}', 'EnumController#getField');
And then in your controller method, use the variable to get the correct data from a keyed array:
public function getField($field)
{
$fields = [
'family' => [
'Nuclear Family',
'Joint Family'
],
// ...
];
return $fields[$field];
}
If you want to continue using different methods for every field then Michael's answer is the easiest option, with one caveat. Allowing users to call any method by name on your controller is a security risk. To protect yourself, you should validate the method name against a whitelist.

How to generate ActiveQuery with client defined query string parameters safely in Yii2?

Is there a safe way to generate conditional clause for Yii2 ORM with query string parameters?
For example, we require a list of some food products, filtering by their properties:
GET /food/?weight[>]=1000&calories=[<]=200
And there is a plenty of different properties of the products: weight, calories, quantity, price.
I expect that it's possible to write something like (simplified code):
$query = new \yii\db\Query();
foreach ($_GET as $parameter => $condition){
foreach ($condition as $operator => $value){
$query->where(new SimpleCondition($parameter, $operator, $value));
}
}
But I doubt this approach is safe.
So, there are three questions:
How is it possible to define the properties from url safely? Can we sanitize the query string parameter names (not values) before using in ActiveQuery::where clause?
What's the way to properly define operators like IN, AND, OR, >, <, >=, <=, etc.?
Is there any native Yii2 component for filtering or should I use a third-party module?
Finally, I found the solution.
It appears Yii2 provides such functionality with DataFilter class.
The official documentation of the class and the guide to use it
According to the documentation
Define a model for validation.
class SearchModel extends \yii\base\Model
{
public $id;
public $name;
public function rules()
{
return [
[['id', 'name'], 'trim'],
['id', 'integer'],
['name', 'string'],
];
}
}
Create the filter:
$filter = new DataFilter(['searchModel' => $searchModel]);
Populate the filter with data, validate
if ($filter->load(\Yii::$app->request->get())) {
$filterCondition = $filter->build();
if ($filterCondition === false) { // if error occure
// the errors are stored in the filter instance
return $filter;
}
}
Use the built condition in a Query filter
$query->andWhere($filterCondition);

Scaffold ListBox multiple select in ModelAdmin Filter for DataObject with Enum

Currently the automatic scaffolding for search fields where there is an enum produces a drop down only allowing one selection to be made. I'm interested in using existing filters to change this to allow multiple selections.
Given the following dataobject...
class MyDataObject extends DataObject {
static $db = array(
'Name' => "Varchar(255)",
'MyEnum' => "Enum('Option1,Option2,Option3','Option1')"
);
}
...and the following ModelAdmin...
class MyModelAdmin extends ModelAdmin {
static $mangaged_models = array(
'MyDataObject',
);
static $url_segment = 'mymodeladmin';
static $menu_title = 'MyModelAdmin';
static $menu_priority = 9;
}
...I'm looking for a module or a simple Filter of some kind to scaffold the Enum into a multiple select listbox
the multiple select listbox is defined as...
Allows multiple selection
After typing some characters suggestions are offered
And I'm asking for a generic solution - I can build a search context for each model admin but this is very frustrating.
Something like the following using either an existing filter (ExactMatchMultiFilter looks perfect but doesn't seem to actually work) or if there is one in a module or someone can suggest how to modify an existing filter for this that would be great.
class MyDataObject extends DataObject {
static $db = array(
'Name' => "Varchar(255)",
'MyEnum' => "Enum('Option1,Option2,Option3','Option1')"
);
public static $searchable_fields = array (
'MyEnum' => array('filter' => 'ExactMatchMultiFilter')
);
}
Any help is much appreciated.
From your question, it seems like you were intending that the filter you pass to the searchable field would change the scaffolding. I've done a bit of digging and that doesn't seem to be the case. However, if you used the field option instead, you can likely achieve what you want.
You do specifically mention ListboxField and while it does support multiple, it isn't enabled by the default constructor on the field which is how it would be instantiated.
What you want could be accomplished out-of-the-box a bit more by the
CheckboxSetField. (I will admit, the UI is a bit average when used in ModelAdmin)
The resulting code could look something like this:
class MyDataObject extends DataObject {
static $db = array(
'Name' => "Varchar(255)",
'MyEnum' => "Enum('Option1,Option2,Option3','Option1')"
);
public static $searchable_fields = array (
'MyEnum' => array('field' => 'CheckboxSetField')
);
}
Unfortunately it isn't that easy, you will notice just by doing that it will come up saying "No options available" instead of a list of checkboxes. This is due to SilverStripe acting differently when we provide the field option that I mentioned earlier.
The workaround for such isn't great but is arguably still generic. I made an extension class of ModelAdmin, it looks for the CheckboxSetField in the search form and sets the Enum values for it.
class MyModelAdminExtension extends Extension {
public function updateSearchForm($form) {
$modelClass = $form->getController()->modelClass;
foreach ($form->Fields() as $field) {
if ($field->class == 'CheckboxSetField') {
//We need to remove the "q[]" around the field name set by ModelAdmin
$fieldName = substr($field->getName(), 2, -1);
$dbObj = singleton($modelClass)->dbObject($fieldName);
if ($dbObj->class == 'Enum') {
$enumValues = $dbObj->enumValues();
$field->setSource($enumValues);
}
}
}
}
}
That is a relatively safe ModelAdmin extension as it specifically looks for the combination of an Enum mapped to a CheckboxSetField which can only happen when you manually specify it.
Having gone this far, we actually could look back at the ListboxField, overcome the multiple option being disabled and populate it with values (as it would suffer the same problem mentioned above). This solution will be a little less generic as we will force all ListboxField's that were mapped from an Enum to be multiples but if we want a nicer solution, this is how we can get it.
class MyModelAdminExtension extends Extension {
public function updateSearchForm($form) {
$modelClass = $form->getController()->modelClass;
foreach ($form->Fields() as $field) {
if ($field->class == 'ListboxField') {
//We need to remove the "q[]" around the field name set by ModelAdmin
$fieldName = substr($field->getName(), 2, -1);
$dbObj = singleton($modelClass)->dbObject($fieldName);
if ($dbObj->class == 'Enum') {
$field->setMultiple(true);
$enumValues = $dbObj->enumValues();
$field->setSource($enumValues);
}
}
}
}
}
And for our model...
class MyDataObject extends DataObject {
private static $db = array(
'Name' => "Varchar(255)",
'MyEnum' => "Enum('Option1,Option2,Option3','Option1')"
);
public static $searchable_fields = array (
'MyEnum' => array('field' => 'ListboxField')
);
}
You now have what you wanted - a multi-select ListBoxField with Enum values.
You might be asking now, why did I cover CheckboxSetField? Well, I think it is important to look at all possible solutions. I came to the solution I provided through trying the CheckboxSetField and it really was only a last minute thing where I realised with some minor modifications, I could get it working for the ListboxField.
As you raised, there is an issue for the above code handling an Enum across a HasOne relationship. This is due to the ModelAdmin extension taking the field name and treating it exclusively like a database field (via dbObject) on the model class of the form. Instead, we can detect a relationship from the dual underscores on the field name, replacing it with dot-syntax and treating that instead like a relationship (via relObject).
Our updated updateSearchForm function would look like this:
public function updateSearchForm($form) {
$modelClass = $form->getController()->modelClass;
foreach ($form->Fields() as $field) {
if ($field->class == 'ListboxField') {
//We need to remove the "q[]" around the field name set by Model Admin
$fieldName = substr($field->getName(), 2, -1);
$dbObj = null;
//Check if the field name represents a value across a relationship
if (strpos($fieldName, '__') !== false) {
//To use "relObject", we need dot-syntax
$fieldName = str_replace('__', '.', $fieldName);
$dbObj = singleton($modelClass)->relObject($fieldName);
}
else {
$dbObj = singleton($modelClass)->dbObject($fieldName);
}
if ($dbObj != null && $dbObj->class == 'Enum') {
$field->setMultiple(true);
$enumValues = $dbObj->enumValues();
$field->setSource($enumValues);
}
}
}
}

Store PHP class settings in variables or return them from methods

I see two different implementations when people handle classes that extend other classes and provide functionality based on certain setting inside the class.
Variables are used to store settings.
Methods are used to return settings.
Using Variables:
class Model {
var $fields = array();
function getFields() {
return array_keys($this->fields);
}
function getRules() {
return $this->fields;
}
}
class Person extends Model {
var $fields = array(
'name' => array('maxLength'=>10),
'email' => array('maxLength'=>50, 'validEmail'=>true),
);
}
Using Methods:
class Model {
function getFields() {}
}
class Person extends Model {
function getFields() {
return array('name','email');
}
function getRules() {
return array(
'name' => array('maxLength'=>10),
'email' => array('maxLength'=>50, 'validEmail'=>true),
);
}
}
Both examples achieve the same results, I can do things like $person->getFields() and $person->getRules(), but in the method-example I don't like the "duplicate" field list, because the fields are actually defined both in $person->getFields() and $person->getRules() and it must compute the array every time it is asked for via the method. On the other hand, I don't like that every object stores all the settings in a variable. It seems like a resource waste. So I'm just wondering what's the better way.
My main questions are:
Is there a performance-reason to pick one way over the other? 2)
Is there a OOP-logic/ease-of-programming/other-reason to pick one
way over the other?
From a few benchmark tests - the times are pretty similar - the exeption though
return array('name','email');
is much faster than
return array_keys($this->fields);
Running 10,000 operations for each method produced these averages:
Variable:
getFields 0.06s
getRules 0.05s
Method:
getFields 0.04s
getRules 0.05s
To answer your second question - it depends on your use-case - if the data stored in these objects is static, or if it will come from another datasource / config file.
One follow up question, why not use object properties?
class Person extends Model {
protected $name
protected $email
public function getName() {
return $this->name;
}
public function getEmail() {
return $this->email;
}
}
My opinion is pick what you are comfortable with, there is no much performance loss or performance gain from using either. You better save the performance saving effort for data handling.
For me I use object properties, it looks clear when you are looking at the class, for storing such default properties, and if you want to override them, then use this beautiful syntax:
array()+array()

Better way to handle value conversion for mixed variable types in an array, PHP

I'm working with a [generally not widely used] PHP framework in which the class I'm currently working with has an array of fields that correlate to columns in SQL.
Well, for setting values for class objects there's a class method setFieldValue and conventionally something like this would be done:
protected $fields = array('id', 'name', 'body');
function setFieldValue($field, $value) {
switch($field) {
case 'id':
return parent::setFieldValue($field, intval($value));
case 'name':
return parent::setFieldValue($field, strval($value));
case 'body':
return parent::setFieldValue($field, strval($value));
}
}
I'm looking for something a bit more dynamic (and cleaner, as I'll have many fields), maybe like:
protected $fields = array('id' => 'intval', 'name' => 'strval', 'body' => 'strval');
function setFieldValue($field, $value) {
if(array_key_exists($field, $this->fields)) {
return parent::setFieldValue($field, $fields[$field]($value));
}
}
Would anyone consider this alternative I'm suggesting bad practice and furthermore would anyone suggest other alternatives?
No, it looks good because the name of the fields aren't promoted to the outside of that class in both cases - the switch or the array_key_exists. So this should not make any difference, because you solve it internally (privately) which is invisible.
Run your unit-tests before and after the changes to see if everything went smoothly.

Categories