I have a model in Yii that contains an array of another model type. I am then trying to validate that no duplicate emails are filled out in a form, where you can fill out for n number of persons at the same time.
My current approach is to trigger a custom validation of the "outer" model that holds all the entrants, however, that model is not accessible in the view, only the array of entrants is, and if I then trigger the error on the "outer" model, it will not be displayed to the user. Therefore I would like to trigger it for the first entrant that violates the rule, but how do I go about doing that?
My code that attempts this, looks like this so far:
/*
* Custom validation rule to hinder the same e-mail being used twice.
*/
public function noRepeatingEmails($attribute, $params)
{
if (!isset($attribute)) return;
$emails = array();
foreach($this->$attribute as $user)
{
if (isset($user) && strlen(trim($user->email)) != 0)
{
$emailToAdd = strtolower(trim($user->email));
if (in_array($emailToAdd, $emails))
{
$this->addError($user, '<my error message>');
return;
}
else
{
$emails[] = $emailToAdd;
}
}
}
}
This only results in a code 500 error though:
Illegal offset type
I presume that is because it is looking for the property "user" in my model, rather than adding an error to "$user" object.
How do I best accomplish this?
I have a .NET background, so I am probably doing loads wrong here however.
If I understood correctly from your comment, you want to validate your model before saving it. For this purpose, CActiveRecord provides beforeSave() method. You need to put this method inside your model:
protected function beforeSave()
{
if(parent::beforeSave())
{
if(/* Your validation goes here*/)
return true;
else
return false
}
else
return false;
}
When the result of this method is true, save() method will be called. Otherwise save() method won't be called and therefore no record will be saved into your database.
Related
Initial setting / premise
I'm trying to figure out what would be the best practice for handling a saving of parent and child model with single controller-action, but handling saving on model-level.
I have an "form"-object which has related model "json_object". This object is passed to controller with AJAX:
form: {
id: 12,
title: "something",
json_object_id: 223, // <-- exists within form -table
json_object: { id: 223, description: 'something else' }
}
Both are represented on database on their own tables and have their own class models. Relations also exists on model-level. Querying of form WITH json_object included as part of results already works.
Current solution
I'v been using lots of static model methods for saving in order to separate saving logic from controller to model. Due to this I'v been passing an object representation of request-input to models save. For an example:
// FormController.php
Form:::saveForm($request->input('form'));
// FormModel.php
public function jsonObject() {
return $this->belongsTo('App\JsonObject');
}
public static function saveForm(Object $in) {
if(isset($in->id) && $in->id > 0) {
$out = Form::find($in->id);
$out->fill((array) $in);
} else {
$out = new Form();
$out->fill((array) $in);
}
// ... any custom saving logics are handled here as well
if(isset($in->json_object)) {
// handle processing of json_object;
}
$out->save();
return $out;
}
With this structure I'm able to pass related model as anonymous object. As "$in" is unrelated to form model class I'm able to access it's anonymous content as well.
Problem / Question
I'v been trying to figure out how this could be archieved by using an instantiated model. On controller level I would most likely do:
public function store(Request $request)
{
if($request->input("form.id") > 0) {
$form = Form::find($request->input("form.id"));
} else {
$form = new Form();
}
$form->fill($request->input("form"));
$form->saveForm();
return response()->json($form);
}
What would be the preferable way to implement processing (saving) of included related model as part of model-level logic? Note: I don't want to introduce JsonObjects saving logic as part of controller logic!
I could pass $json_object as an attribute of saveForm-method, but it sounds poor practice. On the other hand if I represent it as $attribute and $fillable feature of "Form" then saving fails due to attribute not really existing as an column on form-table.
The main goal here is to try to handle most of saving logic on model level. I don't want to represent any model-level logic on controller level, so that's why I want to represent the handling of related model on models method. "SaveForm()"-method would first handle saving of form and then call JsonObject::save() / JsonObject::saveObject(), which would handle processing of JsonObject on its own capsulation (in order to preserve code reusability).
One possible solution
I could add $json_object to formModel.php as ...
public $json_object;
... and manually fill this property on controller.
...
$form->fill($request->input("form"));
$form->json_object = $request->input("form.json_object");
...
// Then use it on formModel:
public function saveForm() {
...
if(isset($this->json_object)) {
// Handle saving logics
dd($this->json_object);
}
NOTE: fill-method doesn't fill json_object to form! $fillable seems to work only against $attributes array, and json_object is not column within forms-table.
Still, is there a better way for handling relations?
For example I have model form MySubmitForm:
use yii\base\Model;
class MySubmitForm extends Model{
public $value1;
public $value2;
public $value3;
public function rules(){
return [['value1', 'value2'],'required'];
}
}
Here I have two required parameters ($value1, $value2) and I want one of them to be required or I want to user got error only when both $value1 and $value2 would be empty.
Can I achieve this from this form model?
You can use Custom validation function inside your validation rules.
Possible options:
1) Select the most important relevant field and add error to it.
2) Select multiple important relevant fields and add the same error message to them (we can store and pass message in a separate variable before passing to keep code DRY).
3) We can use not existing attribute name for adding error, let's say all because attribute existence is not checked at that point.
public function rules()
{
return [['value1', 'value2'],'yourCustomValidationMethod'];
}
public function yourCustomValidationMethod()
{
// Perform custom validation here regardless of "name" attribute value and add error when needed
if ($this->value1=='' && $this->value2=='') {
//either use session flash
Yii::$app->session->setFlash('error','You need to provide one of the fields');
//or use model error without any specific field name
$this->addError('all', 'Your error message');
}
}
Note you can use either session flash or model to notify the error use which every you like
I have a situation where I need a specific attribute accessor appended to one of my models automatically:
class Mission extends Eloquent {
protected $appends = ['launch_date_time'];
public function getLaunchDateTimeAttribute() {
return ($this->attributes['launch_approximate'] == null) ? $this->attributes['launch_exact'] : $this->attributes['launch_approximate'];
}
}
As you can see, this launch_date_time property is dependent on two other fields of my model that are actually in my database.
However, I now want to perform a query where only a certain number of fields are returned, as this is going to be sent over AJAX multiple times and I would rather use as few resources as possible:
// AJAX GET
// missions/all
public function all() {
$allMissions = Mission::with('featuredImage')->get(['mission_id', 'name', 'featured_image']);
return Response::json($allMissions);
}
The issue here is that I no longer need the launch_date_time attribute, so I have excluded it, **in doing so, my AJAX request does not work successfully:
Undefined index: launch_approximate on line 78 of H:\myproj\app\models\Mission.php
This is clearly because my model is attempting to append launch_date_time, of which launch_approximate is a dependency of. If I include all the required dependencies, all of them any my attribute that I want to append appear:
$allMissions = Mission::with('featuredImage')->get(['mission_id', 'name', 'featured_image', 'launch_approximate', 'launch_exact', 'launch_date_time']);
This is undesirable. Is there a solution where I can keep both setups?
The reason it is not working is because you are not retrieving the required fields from the database in the get method on your query. That is why you can't access launch_exact and launch_approximate because they are not set in the instance of your model.
So to make it work like you want. You would have to check if launch_exact and launch_approximate are set before you access them.
public function getLaunchDateTimeAttribute() {
if(isset($this->attributes['launch_approximate']) && $this->attributes['launch_exact']) {
return ($this->attributes['launch_approximate'] == null) ? $this->attributes['launch_exact'] : $this->attributes['launch_approximate'];
}
return null;
}
You can also set a whitelist with the $visible property and a black list with $hidden inside your model to not show certain attributes when outputing to json or a array take a look at the docs: http://laravel.com/docs/5.1/eloquent-serialization#hiding-attributes-from-json
So, I was trying to implement this answer for my other question on the same subject... and it keeps givin me the exceeded time error. Any clues?
This is on my product model. It inherits from Eloquent.
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if( $user_permission->master )
return parent::newQuery();
else if( $user_permission->web_service )
{
$allowed_ids = array();
foreach( $user_permission->allowed_products()->get() as $allowed)
$allowed_ids[] = $allowed->id;
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
If the user is master there is no need to query scope on the request. But, if it isn't then I need to filter by the logged user's permissions.
UPDATE:
I tried the following code in a controller and it works alright:
$user_permission = Auth::user()->permissions;
echo "<PRE>"; print_r($user_permission->allowed_products()->get()); exit;
UPDATE 2:
Guys, I just found out that the problem was in this peace of code:
$allowed = Auth::user()->permissions()->first()->allowed_products()->get()->list('id');
It somehow give me an Maximum execution time of 30 seconds exceeded. If I put the exact same code in a controller, works like a charm, though! I also tried to put it in a scope, also worked. This it's really grinding my gears!
Elloquent has a function called newQuery. Controller does not. When you implement this function in a Model you are overriding the one in Elloquent. If you then invoke Elloquent methods that need a new query for your model before they can return, like ->allowed_products()->get(). Then you are calling your own newQuery() method recursively. Since the user permissions have not changed, this results in infinite recursion. The only outcome can be a timeout because it will keep on trying to determine a filtered product list which causes your newQuery() method to be called, which tries to determine the filtered product list before returning the query, and so on.
When you put the method into a Controller, it is not overriding the Elloquent newQuery method so there is no infinite recursion when trying to get the allowed_product list.
It would be more efficient to apply the filter to the product query based on whether the id is in the user's allowed_products() list using ->whereExists() and build up the same query as allowed_products() except now add condition that id from the query you are filtering is equal to the product id in the allowed products query. That way the filtering is done in the database instead of PHP and all is done in the same query so there is no recursion.
I don't see how your update code works. Illuminate\Database\Eloquent\Collection does not have any magic methods to call the relation functions, you should get an undefined method error trying to do that.
Can you try something like
public function newQuery($excludeDeleted = true)
{
// Returns `Illuminate\Database\Eloquent\Collection`
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
// If here you was to call $user_permission->allowed_products()->get() not much is going to happen, besides probably getting an undefined method error.
$allowed_ids = Auth::user()->permissions()->allowed_products()->get()->lists('id');
return parent::newQuery()->whereIn('id', $allowed_ids);
}
return parent::newQuery();
}
Update: as per comments below I believe the problem is due to newQuery() being called multiple times as the code works just fine when called once in a controller. When this is applied to every query there is no need to collect all the IDs over and over again (assuming they're not going to change each time you call for them). Something such as the below will allow you to store these and only process them once per request rather than every time a query is run.
private $allowed_ids_cache = null;
public function newQuery($excludeDeleted = true)
{
$user_permission = Auth::user()->permissions;
if ($user_permission->master)
{
return parent::newQuery();
}
else if ($user_permission->web_service)
{
if ($this->allowed_ids_cache === null)
{
$this->allowed_ids_cache = Auth::user()->permissions()->allowed_products()->get()->lists('id');
}
return parent::newQuery()->whereIn('id', $this->allowed_ids_cache);
}
return parent::newQuery();
}
I am trying to catch a certain findBy call (with afterFind) where:
if $results is empty (or the value you are trying to find is nonexistent), but the parameter value is found on another table, then it will modify $results to be valid
Some controller action got this:
$this->User->findByUsername("Bingo"); // yes, username Bingo doesnt exist on users table
User model:
function afterFind($results, $primary){
if(empty($results)) {
if(in_array($findbyparameter, array("Bingo", "Bingo1", "Bingo2"))) {
// modify $results
}
}
}
The problem is, how do I get $findbyparameter?
Thanks! All help will be appreciated!
I am not using these convenience methods, but you can pass the variable as Model property like this:
//where you search
$this->User->searchPhrase = "Bingo";
findByUsername($this->User->searchPhrase);
//Model
function afterFind($results, $primary){
if(empty($results)) {
if(in_array($this->searchPhrase, array("Bingo", "Bingo1", "Bingo2"))) {
// modify $results
}
}
}
It's not the prettiest method, but I guess it would work. Try to print_r($this) in afterFind method and see if you can spot somewhere the phrase which you search. I believe it's passed in the condition's array.
Perhaps a custom find type is what you're looking for. Custom find types have two states: before and after.
In the before you would setup your condition, and in the after you would check your data and modify if necessary. In both states you will have access to the query options.
Setting up custom finds is slightly different in 1.x and 2.x (you haven't mentioned which version you're using), so you can look up the specifics in the book.
In short, you would add add your the find type into the $findMethods property of the model and then add the corresponding method name to your model. Say you call your custom find type 'byUsername'
protected function _findByUsername($state, $query, $results = array()) {
if ($state === 'before') {
// add your condition to the query,
return $query;
} elseif ($state === 'after') {
// modify $results if you need to
return $results;
}
}
And you would call it via $this->User->find('byUsername', array('username' => $username));
In $query you would have the key 'username' which you can add to the conditions key of $query. In both states, you would have access to $query['username'].