This has been driving me crazy all day!!
My form creates users. The admin who creates these users can add several rows of inputs and then saves everything at the end. I'm using the clone() method in jQuery and I have already done the client side validation for required inputs etc... I have a very strong Domain layer but now I need server side validation so that Laravel can check if the email already exists since it must be unique. I am very proficient in Laravel, however with the new FormRequest objects I'm stuck on how to loop through each input etc since the FormRequestObject only seems to accept one entry. This all in ajax...
My formData looks like this:
counter:2
_token:KwGAUheSXbzkInh1RZ4RPenx4Fd4fF5DsPm5bjyO
firstname_1:name1
lastname_1:last1
email_1:email#email.com
password_1:keith
firstname_2:name2
lastname_2:last2
email_2:email#email.com
password_2:keith
As you can the input names have an incrementing id so the rules() in form request must loop through these. I'm really stuck, cannot find one example online.
My form reuqest class looks like this:
<?php namespace hidden\Http\Controllers\UserAccess\Requests;
use hidden\Http\Requests\Request;
use Illuminate\Auth\Guard;
use hidden\Domain\Services\UserAccess\GetUserFromEmailService;
class CreateAdministratorRequest extends Request {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'firstname' => 'required',
'lastname' => 'required',
'email' => 'required|email|unique:users,email',
'password' => 'required'
];
}
}
My controller method so far (It will eventually loop through the inputs and dispatched a command but pointless at the moment):
/**
* #param CreateAdministratorRequest $request
*/
public function createAdministrators(CreateAdministratorRequest $request)
{
// dispatch command per entry
}
If you want to keep your structure as possible you could do the following.
public function rules()
{
$counter = $this->get('counter');
$emails= [];
// validate manually if every submitted email is unique
// so, we need every email value
for ($i = 1; $i <= $counter; $i++){
$emails[] = $this->get('email_' . $i);
}
// Now, validate if every email between them are unique
// array_unique() delete repeated values, so we compare
// the size before and after filter the values
$unique = (count($emails) === count(array_unique($emails)));
if (! $unique){
return redirect()->back()
->withMessage('Sorry, all emails should be different');
}
// Now, you need to add every field into the rules
$rules = [];
for ($i = 1; $i <= $counter; $i++){
$rules['email_' . $i] = 'required|email|unique:users,email';
$rules['lastname_' . $i] = 'required';
$rules['firstname_' . $i] = 'required';
$rules['password_' . $i] = 'required';
}
return $rules;
}
Related
I have a notes model. Which has a polymorphic 'noteable' method that ideally anything can use. Probably up to 5 different models such as Customers, Staff, Users etc can use.
I'm looking for the best possible solution for creating the note against these, as dynamically as possible.
At the moment, i'm adding on a query string in the routes. I.e. when viewing a customer there's an "Add Note" button like so:
route('note.create', ['customer_id' => $customer->id])
In my form then i'm checking for any query string's and adding them to the post request (in VueJS) which works.
Then in my controller i'm checking for each possible query string i.e.:
if($request->has('individual_id'))
{
$individual = Individual::findOrFail($request->individual_id_id);
// store against individual
// return note
}elseif($request->has('customer_id'))
{
$customer = Customer::findOrFail($request->customer_id);
// store against the customer
// return note
}
I'm pretty sure this is not the best way to do this. But, i cannot think of another way at the moment.
I'm sure someone else has come across this in the past too!
Thank you
In order to optimize your code, dont add too many if else in your code, say for example if you have tons of polymorphic relationship then will you add tons of if else ? will you ?,it will rapidly increase your code base.
Try instead the follwing tip.
when making a call to backend do a maping e.g
$identifier_map = [1,2,3,4];
// 1 for Customer
// 2 for Staff
// 3 for Users
// 4 for Individual
and so on
then make call to note controller with noteable_id and noteable_identifier
route('note.create', ['noteable_id' => $id, 'noteable_identifier' => $identifier_map[0]])
then on backend in your controller you can do something like
if($request->has('noteable_id') && $request->has('noteable_identifier'))
{
$noteables = [ 'Customers', 'Staff', 'Users','Individual']; // mapper for models,add more models.
$noteable_model = app('App\\'.$noteables[$request->noteable_identifier]);
$noteable_model::findOrFail($request->noteable_id);
}
so with these lines of code your can handle tons of polymorphic relationship.
Not sure about the best way but I have a similar scenario to yours and this is the code that I use.
my form actions looks like this
action="{{ route('notes.store', ['model' => 'Customer', 'id' => $customer->id]) }}"
action="{{ route('notes.store', ['model' => 'User', 'id' => $user->id]) }}"
etc..
And my controller looks this
public function store(Request $request)
{
// Build up the model string
$model = '\App\Models\\'.$request->model;
// Get the requester id
$id = $request->id;
if ($id) {
// get the parent
$parent = $model::find($id);
// validate the data and create the note
$parent->notes()->create($this->validatedData());
// redirect back to the requester
return Redirect::back()->withErrors(['msg', 'message']);
} else {
// validate the data and create the note without parent association
Note::create($this->validatedData());
// Redirect to index view
return redirect()->route('notes.index');
}
}
protected function validatedData()
{
// validate form fields
return request()->validate([
'name' => 'required|string',
'body' => 'required|min:3',
]);
}
The scenario as I understand is:
-You submit noteable_id from the create-form
-You want to remove if statements on the store function.
You could do that by sending another key in the request FROM the create_form "noteable_type". So, your store route will be
route('note.store',['noteableClass'=>'App\User','id'=>$user->id])
And on the Notes Controller:
public function store(Request $request)
{
return Note::storeData($request->noteable_type,$request->id);
}
Your Note model will look like this:
class Note extends Model
{
public function noteable()
{
return $this->morphTo();
}
public static function storeData($noteableClass,$id){
$noteableObject = $noteableClass::find($id);
$noteableObject->notes()->create([
'note' => 'test note'
]);
return $noteableObject->notes;
}
}
This works for get method on store. For post, form submission will work.
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Requests\NoteStoreRequest $request
* #return \Illuminate\Http\Response
*/
public function store(NoteStoreRequest $request) {
// REF: NoteStoreRequest does the validation
// TODO: Customize this suffix on your own
$suffix = '_id';
/**
* Resolve model class name.
*
* #param string $name
* #return string
*/
function modelNameResolver(string $name) {
// TODO: Customize this function on your own
return 'App\\Models\\'.Str::ucfirst($name);
}
foreach ($request->all() as $key => $value) {
if (Str::endsWith($key, $suffix)) {
$class = modelNameResolver(Str::beforeLast($key, $suffix));
$noteable = $class::findOrFail($value);
return $noteable->notes()->create($request->validated());
}
}
// TODO: Customize this exception response
throw new InternalServerException;
}
In my CI4 learning, I have started by trying to simulate user sign in functionality. I have a Controller, two Views (not shown here, but really simply pages- one a pretty much just single form, and the other one a “blank” success HTML page), a set of custom rules in the Validation.php file, and a CustomRule.php file with the first of the methods that will implement all my custom rules (which, ultimately, I’d like to have all set in the Validation.php file). For lack of a better idea, I’ve stuck the CustomRules.php file in the app\Config\ folder.
Here is my problem:
For the life of me, I can’t figure out how to get the Validation service to pass additional parameters (from the form) to my custom rules function called ‘user_validated’. The CI4 documentation describes what the custom function needs to cater for when accepting additional parameters, but not how to trigger the Validation service to pass these additional parameters to one’s custom function… so although ‘user_validated’ is called, only ‘user_email_offered’ is ever passed as in as a string- nothing else goes in, from what I can tell. How do I get around this?
I have tried inserting < $validation->setRuleGroup('user_signin'); > before the call to validate, but found that I could move the setting of the rule group into the call to validate, using: $validationResult = $this->validate('user_signin'), which seemed to do the same, and which doesn't seem to work without the rule-group as a parameter (?). This still doesn't seem to be what triggers the additional data to be passed to the custom rule's method.
Extracts from my hack are appended below.
I’d be very grateful one of you knowledgeable folk could please point me in the right direction.
In app\Controllers\SignupTest.php:
<?php
namespace App\Controllers;
use CodeIgniter\Controller;
class SignupTest extends BaseController
{
public function index() { // redirection from the default to signup(), signin(), ...
return $this->signup();
}
public function signup() {
helper(['form']);
$validation = \Config\Services::validation();
if ($this->request->getPost()) { // still TBD: any different to using $this->request->getGetPost() ?
$validationResult = $this->validate('user_signin'); // set the rules to use: 'user_signin', 'user_signup'
if (!$validationResult) {
$validationErrors = $validation->getErrors();
return view('SignupTestView', $validationErrors); // redisplay simple html form view with list of validation errors
} else {
return view('SignupTestViewSuccess'); // display view to show success
}
} else {
return view('SignupTestView'); // initial display, in the event of there being no POST data
}
}
}
In \app\Config\CustomRules.php:
<?php
namespace Config;
use App\Models\UserModel;
//--------------------------------------------------------------------
// Custom Rule Functions
//--------------------------------------------------------------------
class CustomRules
{
public function user_validated(string $str, string $fields = NULL, array $data = NULL, string &$error = NULL) : bool{
$user_email_offered = $str;
$user_password_offered = ''; // to be extracted using $fields = explode(',', $fields), but $fields is never provided in the call to this user_validated method
if (($user_email_offered !== NULL) && ($user_password_offered !== NULL)) {
$usermodel = new UserModel(); // intended to create a UserEntity to permit connectivity to the database
$user_found = $usermodel->find($user_email_offered); // we're going to assume that user_email is unique (which is a rule configured in the database table)
if ($user_found === NULL) { // check if user exists before doing the more involved checks in the else-if section below, which may throw exceptions if there's nothing to compare (?)
...
}
}
In \app\Config\Validation.php:
?php
namespace Config;
class Validation
{
//--------------------------------------------------------------------
// Setup
//--------------------------------------------------------------------
/**
* Stores the classes that contain the
* rules that are available.
*
* #var array
*/
public $ruleSets = [
\CodeIgniter\Validation\Rules::class,
\CodeIgniter\Validation\FormatRules::class,
\CodeIgniter\Validation\FileRules::class,
\CodeIgniter\Validation\CreditCardRules::class,
\Config\CustomRules::class,
];
/**
* Specifies the views that are used to display the
* errors.
*
* #var array
*/
public $templates = [
'list' => 'CodeIgniter\Validation\Views\list',
'single' => 'CodeIgniter\Validation\Views\single',
];
//--------------------------------------------------------------------
// Custom Rules
//--------------------------------------------------------------------
/* configurable limits for validation rules array below*/
const user_email_min_lenth = 9;
const user_email_max_lenth = 50;
const user_password_min_lenth = 6;
const user_password_max_lenth = 25;
public $user_signin = [
'user_email' => [
'label' => 'e-mail address',
'rules' => 'trim|required|valid_email|user_validated', // user_validated is custom rule, that will have a custom error message
'errors' => [
'required' => 'You must provide an {field}',
'valid_email' => 'Please enter a valid {field}',
]
],
'user_password' => [
'label' => 'password',
'rules' => 'trim|required',
'errors' => [
'required' => 'Enter a {field} to sign in',
'user_password_check' => 'No such user/{field} combination found',
]
Calling custom rule with parameters should be exactly the same as calling CI4's regular rules. Let's get for example "required_without". You use it like in this example:
$validation->setRule('username', 'Username', 'required_without[id,email]');
And the function is declared as so:
public function required_without($str = null, string $fields, array $data): bool
{
$fields = explode(',', $fields);
//...
}
where $str - this is your main field, $fields - string, packing a comma-separated array.
As for Grouping rules, you do not need to group rules to be able to use custom rules with parameters.
If you have only 2 fields to test against you can go a bit cheaper, which will not be perfect but still works:
Function:
public function myrule(string $mainfield, string $fieldtotestwith): bool
{
//doing stuff
}
Validating rule:
$validation->setRule('somemainfield', 'Something', 'myrule[somesecondfield]');
I am using the Lumen Framework, which utilizes the Laravel Validation
I wanted to create a Validator Rule to make the Request->input() json only contain specific keys at the root like "domain" and "nameservers". Not more and not less.
Example passing the rule:
{
"domain":"domain.tld",
"nameservers":
{...}
}
Example not passing the rule:
{
"domain":"domain.tld",
"nameservers":
{...},
"Hack":"executeSomething()"
}
I tried to use to use several default validation rules to achieve this but wasnt successful.
My approach was now to put the request in another array like this
$checkInput['input'] = $request->all();
to make the validator validate the "root" keys.
Now this is my Approach:
create the validator
$checkInput['input'] = $request->all();
$validator = Validator::make($checkInput, [
'input' => [
'onlyContains:domain,nameservers'
],
]);
creating the rule
Validator::extend('onlyContains', function($attribute, $value, $parameters, $validator){
$input = $validator->getData();
$ok = 0;
foreach ($parameters as $key => $value) {
if (Arr::has($input, $attribute . '.' . $value)) {
$ok++;
}
}
if (sizeof(Arr::get($input, $attribute)) - $ok > 0) {
return false;
}
return true;
});
It seems i got the desired result, but i am asking if there is maybe smarter solution to this with the default rules provided by Laravel/Lumen.
You are trying to do a blacklisting approach blocking out fields that are not intended. A simple approach, that is utilized a lot, is to only fetch out the validated. Also you are trying to do logic, that goes against normal validation logic, to do it a field at a time.
This is also a good time, to learn about FormRequest and how you can get that logic, into a place where it makes more sense.
public function route(MyRequest $request) {
$input = $request->validated();
}
With this approach, you will only ever have the validated fields in the $input variable. As an extra bonus, this approach will make your code way easier to pick up by other Laravel developers. Example form request below.
public class MyRequest extends FormRequest
{
public function rules()
{
return [
'domain' => ['required', 'string'],
'nameservers' => ['required', 'array'],
];
}
}
You should use prohibited rule.
For eg:
$allowedKeys = ['domain', 'nameservers'];
$inputData = $request->all();
$inputKeys = array_keys($inputData);
$diffKeys = array_diff($inputKeys, $allowedKeys);
$rules = [];
foreach($diffKeys as $value) {
$rules[$value] = ['prohibited'];
}
I am attempting to add validation to my request validator for a group of fields to make sure that the group of fields does not exceed a certain amount. Once a user submits their form, they can view it in pdf form and my objective is to make sure that the collection of f_name, m_name and l_name does not run into another group of text on the pdf (thus making it illegible).
Essentially, I want something like this:
public function rules()
{
return [
'f_name' + 'm_name' + 'l_name' => 'max:50',
...
];
}
I have searched all over but the only questions I can find related are simply how to do validation. If anyone knows how to group values in validation or links to previously asked questions, please let me know. Thanks mates.
Laravel does not support multifields validation with one rules stack. Try write custom validator or reinvent the wheel:
public function rules()
{
$fields = [
"max: 50" => ["f_name", "m_name", "l_name"]
];
$rules = [];
foreach($fields as $rule => $fieldArray){
if(is_array($fieldArray)){
foreach($fieldArray as $field){
$rules[$field] = $rule;
}
}
}
return $rules;
}
You need a custom validator so you can add several fields.
In your AppServiceProvider, something like this:
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Validator::extend('groupedFieldsLongerThan50', function($attribute, $value, $parameters, $validator) {
$f_name = array_get($this->data, $parameters[0]);
$m_name = array_get($this->data, $parameters[1]);
$l_name = array_get($this->data, $parameters[2]);
if((strlen($f_name)+strlen($m_name)+strlen($l_name))<=50)
return true;
return false;
});
}
...
Where you are declaring your rules add:
'f_name' => 'groupedFieldsLongerThan50:f_name,m_name,l_name'
Is there a way to modify input fields inside a form request class before the validation takes place?
I want to modify some input date fields as follows but it doesn't seem to work.
When I set $this->start_dt input field to 2016-02-06 12:00:00 and $this->end_dt to 2016-02-06 13:00:00 I still get validation error "end_dt must be after start_dt". Which means the input request values aren't getting changed when you update $this->start_dt and $this->end_dt inside the rules() function.
public function rules()
{
if ($this->start_dt){
$this->start_dt = Carbon::createFromFormat('d M Y H:i:s', $this->start_dt . ' ' . $this->start_hr . ':'. $this->start_min . ':00');
}
if ($this->end_dt){
$this->end_dt = Carbon::createFromFormat('d M Y H:i:s', $this->end_dt . ' ' . $this->end_hr . ':'. $this->end_min . ':00');
}
return [
'start_dt' => 'required|date|after:yesterday',
'end_dt' => 'required|date|after:start_dt|before:' . Carbon::parse($this->start_dt)->addDays(30)
];
}
Note: start_dt and end_dt are date picker fields and the start_hr, start_min are drop down fields. Hence I need to create a datetime by combining all the fields so that I can compare.
As of laravel 5.4 you can override the prepareForValidation method of the ValidatesWhenResolvedTrait to modify any input. Something similar should be possible for laravel 5.1.
Example in your Request
/**
* Modify the input values
*
* #return void
*/
protected function prepareForValidation() {
// get the input
$input = array_map('trim', $this->all());
// check newsletter
if (!isset($input['newsletter'])) {
$input['newsletter'] = false;
}
// replace old input with new input
$this->replace($input);
}
The FormRequest has a method validationData() that return the data to validate, so i'm overriding it in my form request:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MyClassRequest extends FormRequest
{
...
/**
* Get data to be validated from the request.
*
* #return array
*/
public function validationData() {
return array_merge(
$this->all(),
[
'number' => preg_replace("/[^0-9]/", "", $this->number)
]
);
}
...
}
It work fine i'm using Laravel 5.4 :)
Try these steps:
1- Middleware
first of all you should create a middleware in app/Http/Middleware:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TransformsRequest;
class DateTimeMiddleware extends TransformsRequest
{
protected $fields = [
'birth_date' => 'toGregorian',
'created_at' => 'toDateTime',
];
protected function transform($key, $value)
{
if (!array_key_exists($key, $this->fields)) {
return $value;
}
$function = $this->fields[$key];
return call_user_func($function, $value);
}
}
with this middleware you can define fields that you want to manipulate them before calling validation and call a specific function to manipulate them.
note: i defined toGregorian and toDateTime in my own helper functions. you can handle it with your own functions
2- Kernel
then modify Http/Kernel.php like bellow:
protected $middlewareGroups = [
'web' => [
...
\App\Http\Middleware\EnglishStrings::class,
],
'api' => [
...
],
];
You can do something like the following:
public function rules(Request $request)
{
if ($request->has('start_dt')){
$request->replace('start_dt', Carbon::createFromFormat('d M Y H:i:s', $request->start_dt . ' ' . $request->start_hr . ':'. $request->start_min . ':00'));
}
if ($request->has('end_dt')){
$request->replace('end_dt' ,Carbon::createFromFormat('d M Y H:i:s', $request->end_dt . ' ' . $request->end_hr . ':'. $request->end_min . ':00'));
}
return [
'start_dt' => 'required|date|after:yesterday',
'end_dt' => 'required|date|after:start_dt|before:' . Carbon::parse($request->start_dt)->addDays(30)
];
}
I took an alternative approach to this, as I want to be able to use $model->fill($validated); in my controllers. As such, I need to ensure that checkboxes have been included as false where they would otherwise be excluded from the array.
So, I created a trait, in app\Traits\ConvertBoolean.php, as follows:
<?php
namespace App\Traits;
trait ConvertBoolean
{
// protected $boolean_attributes = [];
/**
* Override the FormRequest prepareForValidation() method to
* add boolean attributes specified to the request data, setting
* their value to the presence of the data in the original request.
*
* #return void
*/
protected function prepareForValidation() {
if (isset($this->boolean_attributes) && is_array($this->boolean_attributes)) {
$attrs_to_add = [];
foreach ($this->boolean_attributes as $attribute) {
$attrs_to_add[$attribute] = $this->has($attribute);
}
$this->merge($attrs_to_add);
}
}
}
This trait looks for the existence of an array $this->boolean_attributes in the request. If it finds it, it goes through each one and adds the attribute to the request data with the value set to the presence of the attribute in the original request data.
It disregards the value of the HTML form's checkbox value. In most cases, this won't be a problem, but you could change the logic in the trait to look for specific values, if required.
Now that I have this trait, I can then use it in any request, like so:
use App\Traits\ConvertBoolean;
class MyUpdateRequest extends FormRequest
{
use ConvertBoolean;
protected $boolean_attributes = ['my_checkbox'];
// ... other class code
public function rules()
{
// Note that the data is added to the request data,
// so you still need a validation rule for it, in
// order to receive it in the validated data.
return [
'my_checkbox' => 'boolean',
// other rules
];
}
}
If you want to use $this->prepareForValidation() in your request, this is still possible.
Change MyRequest, as follows:
use App\Traits\ConvertBoolean;
class MyUpdateRequest extends FormRequest
{
use ConvertBoolean {
prepareForValidation as traitPrepareForValidation;
}
protected function prepareForValidation() {
// the stuff you want to do in MyRequest
// ...
$this->traitPrepareForValidation();
}
// ...
}