YII: Validate does not populate model unless there is a rule? - yii

What am I missing here. I've got a model with a bunch of variables:
class Car extends CFormModel
{
public $item1;
public $item2;
}
If I post the form with item1 = "one" and item2 = "two" and I do the following in the controller:
if(isset($_POST['Car']))
{
$model->attributes = $_POST['Car'];
if($model->validate()) {
print_r($model);
...
...
At the point where I print the model, none of the items have values. But I add this to my model:
public function rules()
{
return array( array('item1', 'required'));
}
Then item1 populates, but not item2. How then do you get the values for OPTIONAL fields?

If you do not assign any validation rules to property then this property is "unsafe". If property is unsafe then you cannot mass assign anything there, however you can directly assign the variable.
$model->item1 = $_POST{'car']['item1'];
If you are trying to mass assign variables you should also see a warning in Yii trace log.
To read property, read it directly from property
var_dump($model->item1);

Related

Send JSON with list to Controller

I am trying to send following JSON to my controller:
[
{
"collection": "col1",
"uuid": [
"11:22:33:44:55:66",
"11:22:33:44:55:66"
]
},
{
"collection": "test"
}
]
Every object "collection" contains a list of strings symbolized by the uuids.
My model looks like this:
public class DummyDeviceApiModel {
[Display(Name = "UUID")]
[StringLength(36)]
public List<string> uuid {get; set;}
[Display(Name = "Collection")]
public string collection {get; set;}
}
and my controller function like this:
public async Task<ActionResult<DummyDeviceModel>> PostCreateDummyDevice(List<DummyDeviceApiModel> ddpm)
What works is when I just send the collection part, but the UUID with its list makes problem:
System.InvalidCastException: Unable to cast object of type
'System.Collections.Generic.List`1[System.String]' to type
'System.String'
.
Any idea what I am doing wrong? The issue seems to be the "second" list in the model.
Thanks
Stephan
[StringLength(36)] is a validation attribute for type string not arrays of string or List<string>. The exception is probably happening behind the scenes as it tries to validate on your property since it is of type List<string>. If you want to do what I think you do, create a custom validation on your list to make sure you only have strings in your list which are less than or equal to a length of 36, then you need to either implement IValidatableObject on your model (class) or create a custom Validation Attribute for validation by creating a class which inherits from ValidationAttribute. You can read more about how to implement this interface and/or create a custom attribute at MSDN: (https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-3.1). As a test to see if it is indeed the StringLength attribute which is throwing the exception, comment out that particular Validation Attribute in your code and see if it runs without error.

Extend Aurelia Validation Rules on a per class basis

If I have a class
export class Person {
public name: string = "";
public foo: string = "";
}
ValidationRules
.ensure((p :Person) => p.name)
.required()
.withMessage("name is required")
.on(Person);
Is there any way to extend those rules on a per controller basis? For example in my App class I also want to ensure the foo property is set, however adding this as a rule here seems to override the name rule from the above code.
export class App {
public person: Person = new Person();
#observable
public message: string = "";
constructor(public vc: ValidationController, public vld: Validator) {
ValidationRules
.ensure((p: Person) => p.foo).required().withMessage("foo is required").on(this.person);
this.vc.addObject(this.person);
this.vc.validate();
}
}
Yes that's possible, but it requires a slightly different approach.
There are 2 important things to note here:
The fluent api initializer (static method ensure() on ValidationRules) always instantiates a new FluentEnsure object. It doesn't look for existing stuff - not even if you finalize on the same target. To extend a ruleset with more rules, you need to call .ensure() on the existing ruleset.
The finalizer (instance method on() on FluentEnsure) stores the rules in the .prototype.__rules__ (if it's a Function) or .__rules__ property of the target you pass in and will overwrite any existing one.
In other words when you finalize on an instance of Person, you're storing a brand new rules object on person.__rules__ which effectively hides Person.prototype.__rules__.
For the record, the static methods Rules.set(target, rules) and Rules.get(target) are wrappers around the .__rules__ property. You'll definitely want to call those and not try to access the property directly.
Now you might think something like Rules.get(Person).ensure(...).on(person) but that would also modify the original rules on Person.prototype.
So how to work with that?
Enter tags
It can get messy with fetching and combining rulesets, but here's the basic idea:
export class Person {}
ValidationRules.ensure(p => p.name).required().on(Person)
And elsewhere:
Rules.get(Person).ensure(p => p.foo).required().tag("foo");
And elsewhere:
Rules.get(Person).ensure(p => p.bar).required().tag("1234");
Then when it's time to validate:
const allRules = Rules.get(Person); // "1234" is included
const fooRules = ValidationRules.untaggedRules(allRules)
.concat(ValidationRules.taggedRules(allRules, "foo"); "1234" not included
vc.addObject(person);
vc.validate({ fooRules });
Or hand roll something
I've never actually used tags myself before and I've seen an issue or two regarding them. You could also do something similar yourself if you want more control/transparency:
export class Person {
public static scopedRules: { [scope: string]: any } = Object.create(null);
}
ValidationRules.ensure(p => p.name).required().on(Person)
And elsewhere:
Person.scopedRules.foo = Object.create(null);
ValidationRules.ensure(p => p.foo).required().on(Person.scopedRules.foo)
Then when it's time to validate:
const rules = Rules.get(Person).concat(Rules.get(Person.scopedRules.foo));
vc.addObject(person);
vc.validate({ rules });
Of course this is just a "simplest possible thing" example. In a real world scenario you'd probably have your rule storage/retrieval/merging etc tucked away somewhere.

Bug with my relation HasMany/BelongsTo

I have a model Work with this relation
public function types()
{
return $this->belongsTo('App\Models\Type');
}
And a model Type with this relation
public function works()
{
return $this->hasMany('App\Models\Work');
}
I try to access in my view show view to type but I've a lot of errors
Undefined property: Illuminate\Database\Eloquent\Relations\BelongsTo::$name
I try this : $work->types()->name for get data.
In my DB, my table 'Works' have a foreignkey 'type_id'.
I would like to get the 'type' of the post. There can be only one per post.
Thank you very much !
Semantically you want to make your relationships like so:
Work
// A work is of a single type
public function type()
{
return $this->belongsTo('App\Models\Type');
}
Type
// A type of work can have many items of work
public function works()
{
return $this->hasMany('App\Models\Work');
}
You can then access the relationship like so:
$type = Work::first()->type // return object of type Type
$works = Type::first()->works // return collection of objects of type Work
EDIT
By accessing the relationship with () you are returning the underlying query builder instance of the relationship and you will need to finish your statement with ->get() like so:
$works = Type::first()->works()->get();
You should have on Work Model:
public function type()
{
return $this->belongsTo('App\Models\Type');
}
and on your view:
$work->type->name;
Since you are not using default id as foreign key you should add
protected $primaryKey = "type_id";
in your model

An attribute argument must be a constant expression , typeof expression or array creation expression of an attribute parameter type

I want to assign Group name as an attribute for authorize filter.
It will take as below
[FilterConfig.AuthorizeAd(Group = "DirectoryName")]
public ActionResult GetData()
{
}
Insted of hardcoding i tried by adding it as below
[FilterConfig.AuthorizeAd(Group = Constants.ActiveDirectoryName)]
Where Constants is class and created member as below:
public const string ActiveDirectoryName = "directoryName";
Now i want to aceess it from app.config, tried as below
[FilterConfig.AuthorizeAd(Group = ConfigurationManager.AppSettings["Directory_Name"].ToString()
It is throughing the error msg as "An attribute argument must be a constant expression"
How can i assign the data from config?
Please suggest me.
You can't do that with attributes, they have to be constants as stated in the error message. If you wanted to get a value from the configuration file, you could do it by passing the key to the attribute, and then in the constructor get the value you want from the configurationmanager
public MyAttribute :Attribute
{
private string _config;
public MyAttribute(string configKey)
{
_config = ConfigurationManager.AppSettings[configKey];
...
}
}
HTH

Save data from a form collection

I have been following http://framework.zend.com/manual/2.1/en/modules/zend.form.collections.html and it works great with validation and so on.
When the form is valid the guide just runs a var_dump on the entity and it looks something like this:
object(Application\Entity\Product)[622]
protected 'name' => string 'Chair' (length=5)
protected 'price' => string '25' (length=2)
protected 'categories' =>
array (size=2)
0 =>
object(Application\Entity\Category)[615]
protected 'name' => string 'Armchair' (length=8)
1 =>
object(App1ication\Entity\Category)[621]
protected 'name' => string 'Office' (length=6)
The categories can be more then 2 or just 1. How to save a normal form to a database table I understand and have no problem with. But here we have data for two different tables. I guess I could manually read the categories in my controller and fill them in to a model and save them row by row. But that doesn't feel like the best way of doing it.
How do I get the data from the entity to a model or my database? Can it be done without Doctrine?
You have two choices: getData() or bind().
bind() is the "automatic" way - you bind an entity to your form object which has a property on that entity which matches the name of your collection. Then, when the form's isValid() method is called, the binding mechanism will pass the values from the collection's elements to the matching property on the entity.
Alternatively, you can use getData() on the collection object and then do whatever you need to.
Once you have an entity, to save it, consider using ZfcBase as that does the hard work for you.
This is a simple example mapper:
namespace MyModule\Mapper;
use ZfcBase\Mapper\AbstractDbMapper;
use Zend\Stdlib\Hydrator\ArraySerializable;
use MyModule\Entity\MyEntity;
class MyMapper extends AbstractDbMapper
{
protected $tableName = 'my_table';
public function __construct()
{
$this->setHydrator(new ArraySerializable());
$this->setEntityPrototype(new MyEntity());
}
public function save(MyEntity $entity)
{
if (!$entity->getId()) {
$result = $this->insert($entity);
$entity->setId($result->getGeneratedValue());
} else {
$where = 'id = ' . (int)$entity->getId();
$this->update($entity, $where);
}
}
public function fetchAll($choiceGroupId)
{
$select = $this->getSelect($this->tableName);
return $this->select($select);
}
public function loadById($id)
{
$select = $this->getSelect($this->tableName)
->where(array('id' => (int)$id));
return $this->select($select)->current();
}
}
This mapper is using the ArraySerializable hydrator, so your entity object (MyEntity in the example) must implement the methods getArrayCopy() and populate(). getArrayCopy() returns an array of data to be saved and populate() is used to fill the entity from an array of data from database.