I find something lost...
The problem
I need to construct 2 custom FormRequest from 1 normal Request
Let's suppose this fake scenario
First FormRequest
StoreClientRequest
Second FormRequest
UpdateClientRequest
On the Controller:
public function store(Request $request){
//Do something...
$firstRequest = new StoreClientRequest($request);
$secondRequest = new UpdateClientRequest($request);
}
Are there way to make something similar to this fake scenario.
If you really want to get an instance of your Form Requests, without resolving them from the IoC Container, you could use the createFrom method:
$firstRequest = StoreClientRequest::createFrom($request);
This will make sure it is filled with the same data as $request.
Related
I'm trying to use Symfony Voters and Controller Annotation to allow or restrict access to certain actions in my Symfony 4 Application.
As an example, My front-end provides the ability to delete a "Post", but only if the user has the "DELETE_POST" attribute set for that post.
The front end sends an HTTP "DELETE" action to my symfony endpoint, passing the id of the post in the URL (i.e. /api/post/delete/19).
I'm trying to use the #IsGranted Annotation, as described here.
Here's my symfony endpoint:
/**
* #Route("/delete/{id}")
* #Method("DELETE")
* #IsGranted("DELETE_POST", subject="post")
*/
public function deletePost($post) {
... some logic to delete post
return new Response("Deleting " . $post->getId());
}
Here's my Voter:
class PostVoter extends Voter {
private $attributes = array(
"VIEW_POST", "EDIT_POST", "DELETE_POST", "CREATE_POST"
);
protected function supports($attribute, $subject) {
return in_array($attribute, $this->attributes, true) && $subject instanceof Post;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
... logic to figure out if user has permissions.
return $check;
}
}
The problem I'm having is that my front end is simply sending the resource ID to my endpoint. Symfony is then resolving the #IsGranted Annotation by calling the Voters and passing in the attribute "DELETE_POST" and the post id.
The problem is, $post is just a post id, not an actual Post object. So when the Voter gets to $subject instanceof Post it returns false.
I've tried injecting Post into my controller method by changing the method signature to public function deletePost(Post $post). Of course this does not work, because javascript is sending an id in the URL, not a Post object.
(BTW: I know this type of injection should work with Doctrine, but I am not using Doctrine).
My question is how do I get #IsGranted to understand that "post" should be a post object? Is there a way to tell it to look up Post from the id passed in and evaluated based on that? Or even defer to another controller method to determine what subject="post" should represent?
Thanks.
UPDATE
Thanks to #NicolasB, I've added a ParamConverter:
class PostConverter implements ParamConverterInterface {
private $dao;
public function __construct(MySqlPostDAO $dao) {
$this->dao = $dao;
}
public function apply(Request $request, ParamConverter $configuration) {
$name = $configuration->getName();
$object = $this->dao->getById($request->get("id"));
if (!$object) {
throw new NotFoundHttpException("Post not found!");
}
$request->attributes->set($name, $object);
return true;
}
public function supports(ParamConverter $configuration) {
if ($configuration->getClass() === "App\\Model\\Objects\\Post") {
return true;
}
return false;
}
}
This appears to be working as expected. I didn't even have to use the #ParamConverter annotation to make it work. The only other change I had to make to the controller was changing the method signature of my route to public function deletePost(Post $post) (as I had tried previously - but now works due to my PostConverter).
My final two questions would be:
What exactly should I check for in the supports() method? I'm currently just checking that the class matches. Should I also be checking that $configuration->getName() == "id", to ensure I'm working with the correct field?
How might I go about making it more generic? Am I correct in assuming that anytime you inject an entity in a controller method, Symfony will call the supports method on everything that implements ParamConverterInterface?
Thanks.
What would happen if you used Doctrine is that you'd need to type-hint your $post variable. After you've done that, Doctrine's ParamConverter would take care of the rest. Right now, Symfony has no idea how about how to related your id url placeholder to your $post parameter, because it doesn't know which Entity $post refers to. By type-hinting it with something like public function deletePost(Post $post) and using a ParamConverter, Symfony would know that $post refers to the Post entity with the id from the url's id placeholder.
From the doc:
Normally, you'd expect a $id argument to show(). Instead, by creating a new argument ($post) and type-hinting it with the Post class (which is a Doctrine entity), the ParamConverter automatically queries for an object whose $id property matches the {id} value. It will also show a 404 page if no Post can be found.
The Voter would then also know what $post is and how to treat it.
Now since you are not using Doctrine, you don't have a ParamConverter by default, and as we just saw, this is the crucial element here. So what you're going to have to do is simply to define your own ParamConverter.
This page of the Symfony documentation will tell you more about how to do that, especially the last section "Creating a Converter". You will have to tell it how to convert the string "id" into a Post object using your model's logic. At first, you can make it very specific to Post objects (and you may want to refer to that one ParamConverter explicitly in the annotation using the converter="name" option). Later on once you've got a working version, you can make it work more generic.
The question is already asked here for a previous version of laravel and not yet answered.
I have a html form which is validated using three different Form Request Validations. I am able to do this. But, the problem is, the form validations take place one by one. Not at the same time.
If the first form request throws a validation error the form is returned to the view so rest of the two forms doesn't evaluated, hence a proper validation error can't be displayed to the user.
What I want is : validate the form with the three form validation requests rules at the same time.
Controller:
public function store(TransportationRequest $request1, PurchaseRequest $request2, SaleRequest $request3)
{
//do actions here
}
I have tried with inheriting the form requests one by one but could not be succeeded.
Edit :
To be more specific to my question:
I do have three seperate forms for purchase, transporataion and sale which are individually valuated using PurchaseRequest, TransportationRequest and SaleRequest for individual operations.
But there is a special case where a single form handles a purchase, transporataion and a sale. I want to validate the form using combining the three form request rules because I didn't want to write the same validation rules again.
This
Note : The fields in the seperate forms and combined form are same.
Thanks..
A FormRequest throws an Illuminate\Validation\ValidationException Exception when validation fails which has a redirectTo method, and from there the Exception Handler performs the redirect.
You can achieve your desired behaviour by running your Form Requests manually in your controller within a try/catch block which captures the errors and combines the error bags before redirecting, or if it's essential that you run them by Laravel injecting them into your controller then you would need to add your own exception handler which captures all of the errors, combines them and then redirects after the final Form Request has ran.
However, it's worth noting, both of those approaches aren't great: they're cumbersome and are going to cause you more problems than they solve. You should try to adhere to the Laravel way of doing things as best possible if you'd like to write a maintainable application.
A Form Request exists to validate a form, therefore, each Form should have one Form Request, if you wish to compose a Form Request from different sets of rules then that should be done within the Form Request, e.g:
Define your Form Request for your form php artisan make:request StoreMultipleForm
From the rules method on StoreMultipleForm fetch the rules for each of the other Form Requests and then return them together, e.g:
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
$formRequests = [
TransportationRequest::class,
PurchaseRequest::class,
SaleRequest::class
];
$rules = [];
foreach ($formRequests as $source) {
$rules = array_merge(
$rules,
(new $source)->rules()
);
}
return $rules;
}
Use the new composed Form Request in your controller, e.g:
public function store(StoreMultipleForm $request)
{
// Do actions here.
}
The advantages of this method are that it's self-contained, it adheres to the one form one Form Request expectation, it doesn't require changes to the Form Requests you're combining and if you need to add additional rules unique to this form you can do so without creating another Form Request.
I would create traits containing the rules for each FormRequest - purchase, transporataion and sale. Use the trait in it's specific FormRequest and then when you need all the rules you can use all three traits in the combined FormRequest and merge the rules arrays then.
If I understand correctly, you have 3 forms, each with their own form requests to deal with their respective validation. You also have another form which combines those 3 forms somewhere else and you don't want to repeat yourself by rewriting those validation rules.
In which case, I would still suggest going with a single form request, but try to combine the rules of each of those individual requests. For example, you use static methods to define your rules on the 3 individual form requests and have each individual request call its own static method to grab them:
class TransportationRequest extends FormRequest
{
public static function getRules()
{
return []; // Return rules for this request
}
public function rules()
{
return static::getRules();
}
}
class PurchaseRequest extends FormRequest
{
public static function getRules()
{
return []; // Return rules for this request
}
public function rules()
{
return static::getRules();
}
}
class SaleRequest extends FormRequest
{
public static function getRules()
{
return []; // Return rules for this request
}
public function rules()
{
return static::getRules();
}
}
And then have your combined request merge all three sets:
class CombinedRequest extends FormRequest
{
public function rules()
{
return array_merge(
TransportationRequest::getRules(),
SaleRequest::getRules(),
PurchaseRequest::getRules()
);
}
}
Then you can use the single CombinedRequest in your controller method. Of course, if you don't like the static method approach, in your combined request rules method you could just new up each individual request and call the rules method on each of them and merge the results.
class CombinedRequest extends FormRequest
{
public function rules()
{
$transportation = (new TransportationRequest())->rules();
$sale = (new SaleRequest())->rules();
$purchase = (new PurchaseRequest())->rules();
return array_merge(
$transportation,
$sales,
$purchase
);
}
}
Turns out if you resolve the request, validation will kick in.
One caveat of this is that all the validation objects will not kick in at once but depending on your use case, this might be simpler.
public function store()
{
// request()->replace([ some fields to be validated ]);
resolve(TransportationRequest::class);
resolve(PurchaseRequest::class);
resolve(SaleRequest::class);
// request is valid
}
You can manually execute Request classes one by one:
public function store()
{
$isValid = true;
try{
app(TransportationRequest::class);
} catch (Illuminate\Validation\ValidationException $ex){
$isValid = false ;
}
try{
app(PurchaseRequest::class);
} catch (Illuminate\Validation\ValidationException $ex){
$isValid = false ;
}
try{
app(SaleRequest::class);
} catch (Illuminate\Validation\ValidationException $ex){
$isValid = false ;
}
if (!$isValid){
throw $ex;
}
}
If validation in one of them will fail, a user will be redirected back to previous page with an error.
You could concatinate all rules and validate manually:
$allRules = (new TransportationRequest)->rules() + (new PurchaseRequest)->rules() + (new SaleRequest)->rules();
Validator::make($request->all(), $allRules)->validate();
I know this is a pretty old question, but I got annoyed by not being able to chain form requests together so I made a composer package for this, so you don't have to.
https://github.com/sharpie89/laravel-multiform-request
I recently came up against this problem, and solved it like this:
public function rules()
{
$request1 = RequestOne::createFrom($this);
$request2 = RequestTwo::createFrom($this);
return array_merge(
$request1->rules(),
$request2->rules()
);
}
So I'm working on an admin interface. I have a route set up like so:
Route::controllers([
'admin' => 'AdminController',
]);
Then I have a controller with some methods:
public function getEditUser($user_id = null)
{
// Get user from database and return view
}
public function postEditUser($user_id = 0, EditUserRequest $request)
{
// Process any changes made
}
As you can see, I'm using method injection to validate the user input, so URL's would look like this:
http://example.com/admin/edit-user/8697
A GET request would go to the GET method and a POST request to the POST method. The problem is, if I'm creating a new user, there won't be an ID:
http://examplecom/admin/edit-user/
Then I get an error (paraphrased):
Argument 2 passed to controller must be an instance of EditUserRequest, none given
So right now I'm passing an ID of 0 in to make it work for creating new users, but this app is just getting started, so am I going to have to do this throughout the entire application? Is there a better way to pass in a validation method, and optionally, parameters? Any wisdom will be appreciated.
You can reverse the order of your parameters so the optional one is a the end:
public function postEditUser(EditUserRequest $request, $user_id = null)
{
}
Laravel will then resolve the EditUserRequest first and pass nothing more if there's no user_id so the default value will kick in.
I am using Symfony with propel to generate a form called BaseMeetingMeetingsForm.
In MeetingMeetingsForm.class.php I have the following configure method:
public function configure() {
$this->useFields(array('name', 'group_id', 'location', 'start', 'length'));
$this->widgetSchema['invited'] = new myWidgetFormTokenAutocompleter(array("url"=>"/user/json"));
}
In MeetingMeetings.php my save method is simply:
public function save(PropelPDO $con = null) {
$this->setOwnerId(Meeting::getUserId());
return parent::save($con);
}
However propel doesn't know about my custom field and as such doesn't do anything with it. Where and how to I put in a special section that can deal with this form field, please be aware it is not just a simple save to database, I need to deal with the input specially before it is input.
Thanks for your time and advice,
You have to define a validator (and/or create your own). The validator clean() method returns the value that needs to be persisted.
In Doctrine (I don't know Propel) the form then calls the doUpdateObject() on the form, which in turns calls the fromArray($arr) function on the model.
So if it's already a property on your model you'll only need to create the validator. If it's a more complex widget, you'll need to add some logic to the form.
I'm building an app for the company I work for in symfony, non the less the app might be pretty useful outside our company so we decided to write it in a more general form so we can make it multi company. I'm facing a problem on how to define a default value for a field that is going to be in every single model (company_id) so we don't need to select which company we belong to every time we want to add data. can anyone help me?
I've tried
class TestForm extends BaseTestForm
{
function configure()
{
$this->setDefault('company_id', '1');
}
}
and when I submit the form I get a missing value for model ....
I did it, in the action of course, before the processForm and after the $this->form = new TestForm();
I used:
public function executeCreate(sfWebRequest $request)
{
...
$this->form->getObject()->setCompanyId('1');
...
}