Laravel - can I inject a FormRequest inside a FormRequest? - forms

I have a complex set of rules that I build dynamically but ideally I need one set of rules to be validated before I move on to the next, i.e.
I have a json object {data: {}}, which I want to check exists before I move on to validate fields nested in data, which have other nested fields etc etc.
I thought about having one FormRequest class, say ApiRequest, which could validate data, then another, say UploadRequest, which adds further rules on the basis that the data has already been validated.
I can do this, and it works, by injecting ApiRequest inside the constructor of UploadRequest, which allows the full set of rules to be validated in ApiRequest, before moving on to the next set:
/**
* UploadRequest constructor.
* #param ApiRequest $apiRequest
*/
public function __construct(ApiRequest $apiRequest) {
// If we got here, ApiRequest passed validation, woo!
}
Does this seem like an anti pattern or the 'wrong' way to handle this?
I was trying to think of a request being made up of mini requests i.e. an upload request via the api is really an api request, followed by an upload request etc. But I'm not sure if I'm just trying to justify this to myself.
Any help would be greatly appreciated.
Thanks!

You can have one ApiRequest class that validates all mandatory parameters for all endpoints.
class ApiRequests extends FormRequest
{
public function rules()
{
return [
'data' => 'required',
'data.field_1' => 'required|string',
'data.field_2' => 'required|string'
];
}
}
And let's say your API has an endpoint that edits user's profile. And the request made to that endpoint must include first_name and last_name. So a new class that extends ApiRequest can be made:
class EditUserRequest extends ClientRequest
{
public function rules()
{
return array_merge(parent::rules(), [
'params.first_name' => 'required',
'params.last_name' => 'required',
]);
}
}
That will merge all rules from ApiRequest with those required for EditUserRequest.
And then in your controller (eg. UsersController):
public function update(EditUserRequest $request) {
.... your code goes here...
}

Related

Symfony 4 setter injection in parent class

Just a quick question. I'm building some API. I was thinking about creating simple parent class that would deal with form requests.
So for example if you would like to easily handle form request you just extend this class and you get access to request object, request data extracted from that object and bunch of methods that do some things for you out of the box. It doesn't matter what and why exactly.
The problem is:
I send request through postman.
I try to use request object in class that extends parent class but instead of request I get null.
How do I set up the whole thing?:
Now in Symfony every controller is by default registered as a service so I override this definition like this:
#generic api form controller
App\Controller\Api\ApiFormController:
calls:
- [setDependencies, ['#request_stack', '#App\Service\Serialization\Serializer']]
So as you can see I am using setter injection.
I extend above class in my other class. Let's call it PostController. So:
<?php
namespace App\Controller\Api;
use Symfony\Component\HttpFoundation\RequestStack;
class ApiFormController
{
/**
* #var Request
*/
public $request;
/**
* #param RequestStack $requestStack
*/
public function setDependencies(
RequestStack $requestStack
) {
$this->request = $requestStack;
}
}
And now PostController:
public function get(int $post = null)
{
dump($this->request); exit;
}
I was expecting to get access like this and I think I understand why I don't have access to this object. I'm looking for some ideas how I could achieve this goal in cleanest possible way. I'm not expecting ready answers but hints.
I was thinking about using events to set it up in the background?
I also think it has something to do with the way I'm hooking up my controller as a service.
The core of it all: Symfony does not pick up service definition for subclasses. So if you define dependencies for a class and extend it in another class, you have to define the dependencies for this second class too.
The easiest way is to use the parent keyword for this, so your example would work in the following way:
App\Controller\Api\ApiFormController:
calls:
- [setDependencies, ['#request_stack', '#App\Service\Serialization\Serializer']]
PostController:
parent: App\Controller\Api\ApiFormController
If you are using autowiring, you can use #required to make Symfony call the setter automatically. https://symfony.com/doc/current/service_container/autowiring.html#autowiring-other-methods-e-g-setters
/**
* #param RequestStack $requestStack
* #required
*/
public function setDependencies(
RequestStack $requestStack
) {
$this->request = $requestStack;
}
This should do the trick.
I see several problems here.
If you want to inject dependencies in such a way you should define controller as service. You can read more here.
Routing should be something like this:
# config/routes.yaml
get_something:
path: /
defaults: { _controller: App\Controller\Api\PostController:get }
Also, you should define PostController as service, not ApiFormController.
You injected RequestStack but type hint for the attribute is Request.
Instead of:
$this->request = $requestStack;
You need to use:
$this->request = $requestStack->getMasterRequest();

Is splitting an index action into multiple ones a restful-friendly approach?

I need to display two different index pages to two different user groups. For example, a regular user should see one page, and a privileged user - another one. I see two ways of approaching this issue:
One index action with conditionals:
public function index()
{
// view for privileged users
if(request()->user()->hasRole('privileged')){
return view('index_privileged');
}
// view for regular users
if(request()->user()->hasRole('regular')){
return view('index_regular');
}
return redirect('/');
}
Multiple actions:
public function index_privileged()
{
return view('index_privileged');
}
public function index_regular()
{
return view('index_regular');
}
Which approach is more "restful-friendly" and generally better?
I'm a big fan of light controllers. This might be a little overboard for a simple problem but if something like this pops up again, you'd already have everything all setup.
With that said, it might be best to create a PrivilegedUser class and a RegularUser class and give them both an index method which returns their respective views. Code them both to an interface UserInterface and make sure they both implement that.
Here is what those looked like in my test.
class RegularUser implements UserInterface
{
public function index()
{
return view('index_regular');
}
}
class PrivilegedUser implements UserInterface
{
public function index()
{
return view('index_privileged');
}
}
interface UserInterface
{
public function index();
}
Then you can add a listener which should run for the event Illuminate\Auth\Events\Login. Laravel will fire this event for you automatically when someone logs in. This goes into the file EventServiceProvider.php.
protected $listen = [
'Illuminate\Auth\Events\Login' => [
'App\Listeners\AuthLoginListener',
],
];
Now you can run php artisan event:generate to generate the new listener. Here is what my listener looks like, it should work for you.
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Application;
class AuthLoginListener
{
/**
* Create the event listener.
*
* #param Application $app
*/
public function __construct(Application $app)
{
$this->app = $app;
}
/**
* Handle the event.
*
* #param Login $event
* #return void
*/
public function handle(Login $event)
{
if ($event->user->hasRole('privileged')) {
$this->app->bind('App\Repositories\UserInterface', 'App\Repositories\PrivilegedUser');
} else if ($event->user->hasRole('regular')) {
$this->app->bind('App\Repositories\UserInterface', 'App\Repositories\RegularUser');
}
}
}
Essentially what this is doing is telling Laravel to load up a certain class based on the type of user that just logged in. The User instance is available through the Login object which was automatically passed in by Laravel.
Now that everything is setup, we barely have to do anything in our controller and if you need to do more things that are different depending on the user, just add them to the RegularUser or PrivilegedUser class. If you get more types of users, simply write a new class for them that implements the interface, add an additional else if to your AuthLoginListener and you should be good to go.
To use this, in your controller, you'd do something like the following...
// Have Laravel make our user class
$userRepository = App::make('App\Repositories\UserInterface');
return $userRepository->index()->with('someData', $data);
Or even better, inject it as a dependency.
use App\Repositories\UserInterface;
class YourController extends Controller
{
public function index(UserInterface $user)
{
return $user->index();
}
}
Edit:
I just realized I forgot the part where you wanted to return redirect('/'); if no condition was met. You could create a new class GuestUser (I know this sounds like an oxymoron) which implements UserInterface but instead of using the AuthLoginListener, I'd bind it in a service provider when Laravel boots. This way Laravel will always have something to return when it needs an implementation of UserInterface in the event it needs this class if no one is logged in.
Well, its more like a refactoring "issue" than a rest-friendly issue. Check this guideline and you can see that most of the things that makes an api friendly is concerned to the url.
But, lets answer what you are asking. The thing you wanna do is a refactoring method but it is not only the move method but something like the extract variable.
The second option would make the code more readable, either ways are right but the second is more developer friendly. It enhances the code readability from any developer. I would recommend using the second option.
Refactoring is never enough, but read something like this, it will help you a lot writing more readable codes.

Yii2: how to use custom validation function for activeform?

In my form's model, I have a custom validation function for a field defined in this way
class SignupForm extends Model
{
public function rules()
{
return [
['birth_date', 'checkDateFormat'],
// other rules
];
}
public function checkDateFormat($attribute, $params)
{
// no real check at the moment to be sure that the error is triggered
$this->addError($attribute, Yii::t('user', 'You entered an invalid date format.'));
}
}
The error message doesn't appear under the field in the form view when I push the submit button, while other rules like the required email and password appear.
I'm working on the Signup native form, so to be sure that it is not a filed problem, I've set the rule
['username', 'checkDateFormat']
and removed all the other rules related to the username field, but the message doesn't appear either for it.
I've tried passing nothing as parameters to checkDateFormat, I've tried to explicitly pass the field's name to addError()
$this->addError('username', '....');
but nothing appears.
Which is the correct way to set a custom validation function?
Did you read documentation?
According to the above validation steps, an attribute will be
validated if and only if it is an active attribute declared in
scenarios() and is associated with one or multiple active rules
declared in rules().
So your code should looks like:
class SignupForm extends Model
{
public function rules()
{
return [
['birth_date', 'checkDateFormat'],
// other rules
];
}
public function scenarios()
{
$scenarios = [
'some_scenario' => ['birth_date'],
];
return array_merge(parent::scenarios(), $scenarios);
}
public function checkDateFormat($attribute, $params)
{
// no real check at the moment to be sure that the error is triggered
$this->addError($attribute, Yii::t('user', 'You entered an invalid date format.'));
}
}
And in controller set scenario, example:
$signupForm = new SignupForm(['scenario' => 'some_scenario']);
Try forcing the validation on empty field
['birth_date', 'checkDateFormat', 'skipOnEmpty' => false, 'skipOnError' => false],
Also, make sure you don't assign id to your birth_date field in your view.
If you do have id for your birth_date, you need to specify the selectors
<?= $form->field($model, 'birth_date', ['selectors' => ['input' => '#myBirthDate']])->textInput(['id' => 'myBirthDate']) ?>
To make custom validations in yii 2 , you can write custom function in model and assign that function in rule.
for eg. I have to apply password criteria in password field then I will write like this in model.
public function rules()
{
return [
['new_password','passwordCriteria'],
];
}
public function passwordCriteria()
{
if(!empty($this->new_password)){
if(strlen($this->new_password)<8){
$this->addError('new_password','Password must contains eight letters one digit and one character.');
}
else{
if(!preg_match('/[0-9]/',$this->new_password)){
$this->addError('new_password','Password must contain one digit.');
}
if(!preg_match('/[a-zA-Z]/', $this->new_password)){
$this->addError('new_password','Password must contain one character.');
}
}
}
}
You need to trigger $model->validate() somewhere if you are extending from class Model.
I stumbled on this when using the CRUD generator. The generated actionCreate() function doesn't include a model validation call so custom validators never get called. Also, the _form doesn't include and error summary.
So add the error summary to the _form.
<?= $form->errorSummary($model); ?>
...and add the validation call - $model->validate() - to the controller action
public function actionCreate()
{
$model = new YourModel();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {...
Although it's an old post i thought I should answer.
You should create a Custom Validator Class and to create a validator that supports client-side validation, you should implement the yii\validators\Validator::clientValidateAttribute() method which returns a piece of JavaScript code that performs the validation on the client-side. Within the JavaScript code.
You may use the following predefined variables:
attribute: the name of the attribute being validated.
value: the value being validated.
messages: an array used to hold the validation error messages for
the attribute.
deferred: an array which deferred objects can be pushed into
(explained in the next subsection).
SO that means you can use messages array to push your messages to the client end on runtime within the javascript code block in this method.
I will create a class that includes dummy checks that could be replaced the way you want them to. and change the namespace according to your yii2 advanced or basic.
Custom Client-side Validator
namespace common\components;
use yii\validators\Validator;
class DateFormatValidator extends Validator{
public function init() {
parent::init ();
$this->message = 'You entered an invalid date format.';
}
public function validateAttribute( $model , $attribute ) {
if ( /*SOME CONDITION TO CHECK*/) {
$model->addError ( $attribute , $this->message );
}
}
public function clientValidateAttribute( $model , $attribute , $view ) {
$message = json_encode ( $this->message , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
return <<<JS
if ($("#DATE-1").val()=="" || $("#DATE-2").val() =="") {
messages.push($message);
}
JS;
}
}
and then inside your model SigupForm add the rule
['birth_date', 'common\components\DateFormatValidator'],
Deferred Validation
You can even add ajax calls inside the clientValidateAttribute function and on the base of the result of that ajax call you can push message to the client end but you can use the deferred object provided by yii that is an array of Deferred objects and you push your calls inside that array or explicitly create the Deferred Object and call its resolve() method.
Default Yii's deferred Object
public function clientValidateAttribute($model, $attribute, $view)
{
return <<<JS
deferred.push($.get("/check", {value: value}).done(function(data) {
if ('' !== data) {
messages.push(data);
}
}));
JS;
}
More about Deferred Validation
You need to render the model from controller. Without initializing the model in view. And in the controller you need to call the validate function
Are you sure the first parameter of addError shouldn't be like this
$this->addError(**'attribute'**, Yii::t('user', 'You entered an invalid date format.'));
I had common problem.
In your validation function:
public function checkDateFormat($attribute, $params)
{
// no real check at the moment to be sure that the error is triggered
$this->addError($attribute, Yii::t('user', 'You entered an invalid date format.'));
}
$params doesn`t get any value at all. It actually always equals to Null. You have to check for your attribute value in function:
public function checkDateFormat($attribute, $params)
{
if($this->birth_date == False)
{
$this->addError($attribute, Yii::t('user', 'You entered an invalid date format.'));
}
}
that`s how it worked for me.
If you don't use scenarios for your model, you must mark your atribute as 'safe':
['birth_date','safe'],
['birth_date', 'checkDateFormat'],
And, on the other hand, you can use this for date validation:
['birth_date','safe'],
[['birth_date'],'date', 'format'=>'php:Y-m-d'],
You can change format as you want.
**We should set attributes to the function to work with input value **
public function rules()
{
return [
['social_id','passwordCriteria'],
];
}
public function passwordCriteria($attribute, $params)
{
if(!empty($this->$attribute)){
$input_value = $this->$attribute;
//all good
}else{
//Error empty value
$this->addError('social_id','Error - value is empty');
}
}
Are you by any chance using client side validation? If you do then you have to write a javascript function that would validate the input. You can see how they do it here:
http://www.yiiframework.com/doc-2.0/guide-input-validation.html#conditional-validation
Another solution would be to disable client validation, use ajax validation, that should bring back the error too.
Also make sure that you have not overwritten the template of the input, meaning make sure you still have the {error} in there if you did overwrite it.
Your syntax on rules should be something like this man,
[['birth_date'], 'checkDateFormat']
not this
['birth_date', 'checkDateFormat']
So in your case, it should look like below
...
class SignupForm extends Model
{
public function rules()
{
// Notice the different with your previous code here
return [
[['birth_date'], 'checkDateFormat'],
// other rules
];
}
public function checkDateFormat($attribute, $params)
{
// no real check at the moment to be sure that the error is triggered
$this->addError($attribute, Yii::t('user', 'You entered an invalid date format.'));
}
}

Laravel REST redirect from GET to POST method in SAME controller not working

I am trying to support the use of EITHER GET or POST methods in my REST controller in laravel.
So, I would like to redirect ANY get requests sent to our REST controller to the POST method in the SAME controller instead.
I have tried many things, and now have returned back to basics as follows:
routes.php
Route::resource('user', 'userController');
userController.php
class userController extends \BaseController {
public function index() {
return Redirect::action('userController#store');
}
public function store() {
echo 'yeeha!';
}
}
Performing a POST on the page works and outputs:
yeeha!
Performing a GET on the page produces:
Could not get any response
This seems to be like an error connecting to https://www.test.com/user. The response status was 0.
Check out the W3C XMLHttpRequest Level 2 spec for more details about when this happens.
I have tried many different redirects and none are successful.
The correct way is to do it is to use the routes file and just define it;
Routes.php
Route::get('/user', array ('as' => 'user.index', 'uses' => userController#store))
Route::post('/user', array ('as' => 'user.create', 'uses' => userController#store))
Controller
class userController extends \BaseController {
public function store() {
echo 'yeeha!';
}
}

CUploadedFile::getInstance Always Returns Null on Uploaded File

I have built a form in YII and I need to process an uploaded file. I followed this guide, but I've stumbled upon an issue. CUploadedFile::getInstance always returns null.
Here's what I use. My model:
class AdditionalFieldFile extends CFormModel {
public $uploadedFile;
/**
* #return array validation rules for model attributes.
*/
public function rules() {
return array(
//note you wont need a safe rule here
array('uploadedFile', 'file', 'allowEmpty' => true, 'types' => 'zip,doc,xls,ppt,jpg,gif,png,txt,docx,pptx,xlsx,pdf,csv,bmp'),
);
}
}
And handling the uploaded file in the controller on form submit:
$model = new AdditionalFieldFile();
$model->uploadedFile = CUploadedFile::getInstance($model, 'field_'.$type_field.'['.$id_common.']');
And after that $model->uploadedFile is null for some reason.
Note that $type_field and $id_common come dynamically.
Also, the form has 'enctype'=>'multipart/form-data' so this is not the cause.
Ok, a self-answer follows.
It turns out that I had to use the active file field in the view, using an instance of the class that extends CFormModel as a model.
Solution:
CHtml::activeFileField(new AdditionalFieldFile(), 'field_'.$type_field.'['.$id_common.']');