How to (properly) use form requests + policies + resource routes with dependencies? - php

My use case is that an user is/owns a company, which has employees.
Using form controllers along with model policies i am trying to figure out what the best/proper way to do it should be.
routes:
Route::resource('company', \App\Http\Controllers\Api\v1\CompanyController::class);
Route::resource('employee', \App\Http\Controllers\Api\v1\EmployeeController::class);
employee store request:
namespace App\Http\Requests;
use App\Models\Employee;
use Illuminate\Foundation\Http\FormRequest;
class EmployeeStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return $this->user()->can('create', Employee::class);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'company_id' => 'required|integer|exists:companies,id'
];
}
}
employee policy:
...
/**
* Determine whether the user can create employees.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
return $user->can('update', Company::find(
app('request')->get('company_id')
));
}
...
So i am not particularly happy in how the policy checks if the user can edit the company the employee will belong to, since this only happens on http, for console/tests this will break.
Then the most logical way to add this check is in the form request's authorize() function, but then you are checking permissions outside the policies, which sounds illogical.
So in short, the question: how & why would you do this using form requests & model policies?

You only need to add the id of the company to the EmployeePolicy#create method, and you will be able to use it outside http
EmployeePolicy
public function create(User $user, $companyId)
{
return $user->can('update', $companyId);
}
EmployeeStoreRequest
public function authorize()
{
return $this->user()->can('create', Employee::class, $this->request->get('company_id'));
}
You can test it outside http with tinker
php artisan tinker
$user = User::find(2); // or whatever user you want to test with
$user->can('create', Employee::class, 3); // 3 = company_id

Related

How do I pass a method with my own constraints in a controller that uses request pattern to display data

Merry Christmass team!
I have a problem trying to figure out how to pass a method with my own constraints in a controller that is bound to request patter paradigm:
Sample Controller Code:
class SampleController
{
protected $model = SampleModel::class;
protected $indexRequest = IndexRequest::class;
}
Request Class
class IndexRequest extends FormRequest
{
/**
* 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 [];
}
Assume I have a method that I want to do something different.Say I want to fetch some data based on some column constraints.
Whats the approach?

Checking FormRequest's validation rules before authorization in Laravel

I'm implementing an API in Laravel using JSON:API specification.
In it I have a resource, let's call it Ponds, with many-to-many relationships with another resource, let's call it Ducks.
According to JSON:API specs in order to remove such relationship i should use DELETE /ponds/{id}/relationships/ducks endpoint, with request of following body:
{
"data": [
{ "type": "ducks", "id": "123" },
{ "type": "ducks", "id": "987" }
]
}
This is handled by PondRemoveDucksRequest, which looks as follows:
<?php
...
class PondRemoveDucksRequest extends FormRequest
{
public function authorize()
{
return $this->allDucksAreRemovableByUser();
}
public function rules()
{
return [
"data.*.type" => "required|in:ducks",
"data.*.id" => "required|string|min:1"
];
}
protected function allDucksAreRemovableByUser(): bool
{
// Here goes the somewhat complex logic determining if the user is authorized
// to remove each and every relationship passed in the data array.
}
}
The problem is that if I send a body such as:
{
"data": [
{ "type": "ducks", "id": "123" },
{ "type": "ducks" }
]
}
, I get a 500, because the authorization check is triggered first and it relies on ids being present in each item of the array. Ideally I'd like to get a 422 error with a standard message from the rules validation.
Quick fix I see is to add the id presence check in the allDucksAreRemovableByUser() method, but this seems somewhat hacky.
Is there any better way to have the validation rules checked first, and only then proceed to authorization part?
Thanks in advance!
1 - Create abstract class called "FormRequest" inside App\Requests directory and override the
validateResolved() method:
<?php
namespace App\Http\Requests;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
abstract class FormRequest extends BaseFormRequest
{
/**
* Validate the class instance.
*
* #return void
* #throws AuthorizationException
* #throws ValidationException
*/
public function validateResolved()
{
$validator = $this->getValidatorInstance();
if ($validator->fails())
{
$this->failedValidation($validator);
}
if (!$this->passesAuthorization())
{
$this->failedAuthorization();
}
}
}
2 - Extend your FormRequests with custom FormRequest
<?php
namespace App\Http\Requests\Orders;
use App\Http\Requests\FormRequest;
class StoreOrderRequest extends FormRequest
{
}
add $this->getValidatorInstance()->validate(); at beggining of authorize() method
The most cleanest solution I found to solve it was by creating a small trait for the FormRequest and use it anytime you want to run validation before the authorization, Check the example bellow:
<?php
namespace App\Http\Requests\Traits;
/**
* This trait to run the authorize after a valid validation
*/
trait AuthorizesAfterValidation
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Set the logic after the validation
*
* #param $validator
* #return void
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
if (! $validator->failed() && ! $this->authorizeValidated()) {
$this->failedAuthorization();
}
});
}
/**
* Define the abstract method to run the logic.
*
* #return void
*/
abstract public function authorizeValidated();
}
Then in your request class:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Traits\AuthorizesAfterValidation;
class SomeKindOfRequest extends FormRequest
{
use AuthorizesAfterValidation;
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorizeValidated()
{
return true; // <---- Set your authorization logic here
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
//
];
}
}
Source https://github.com/laravel/framework/issues/27808#issuecomment-470394076
Here is a slightly different approach than what you are attempting, but it may accomplish the desired outcome for you.
If you are trying to validate whether the given duck id belongs to the user, this can be done in the rule itself as follows:
"data.*.id" => "exists:ducks,id,user_id,".Auth::user()->id
This rule asks if a record exists in the ducks table which matches the id and where the user_id is the current logged in user_id.
If you chain it to your existing rules (required|string|min:1), using 'bail', then it wouldn't run the query unless it had passed the other three rules first:
"data.*.id" => "bail|required|string|min:1|exists:ducks,id,user_id,".Auth::user()->id

Laravel 'required' rule not working on Request

I'm using laravel 5.5
I have a Request that I've built but the required rule is not working correctly.
Route
Route::get('v1/learning_centre/user/{userId}/course/list', 'API\LearningCentre#userCourses');
Controller
public function userCourses(GetUserCourses $request)
{
$courses = User::findOrFail($request->userId)
->courses()
->get();
return new CourseResourceCollection($courses);
}
Request
namespace App\Http\Requests\LearningCentre;
use Illuminate\Foundation\Http\FormRequest;
class GetUserCourses extends FormRequest {
/**
* 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 [
'userId' => 'required|integer'
];
}
/**
* Get the error messages for the defined validation rules.
*
* #return array
*/
public function messages()
{
return [
'userId.required' => 'A User is required',
];
} }
If I turn off the required rule I can get to the controller. If I have the required rule in the request I get a 302. I am passing in a valid userId in phpunit. Without the request rules my code works as intended.
Any ideas?
You should be using route model binding to validate a required GET parameter in this situation, not a FormRequest class, which, as the name should indicate, are intended for form requests.
Your route:
Route::get('v1/learning_centre/user/{user}/course/list', 'API\LearningCentre#userCourses');
Your controller:
public function userCourses(User $user) {
If a user ID is missing (or an invalid one used), your controller will automatically throw a ModelNotFoundException, which Laravel by default returns as a 404.

Request validation allways passes on Laravel using Dingo/Api

I'm using dingo/api package.
Controller:
public function register(RegisterUserRequest $request)
{
dd('a');
}
And for example the email field is required:
<?php namespace App\Http\Requests;
class RegisterUserRequest 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 [
'email' => 'required'
];
}
}
So I send a request without the email, and still getting the "a" response.
I also tried to extend Dingo\Api\Http\Request instead of App\Http\Request, but still the same.
For Dingo to work at all with the FormRequest, by experience (and from this Issue), you have to use Dingo's Form request i.e Dingo\Api\Http\FormRequest; , so you'll have something similar to:
<?
namespace App\Http\Requests;
use Dingo\Api\Http\FormRequest;
use Symfony\Component\HttpKernel\Exception\HttpException;
class RegisterUserRequest extends FormRequest
{
/**
* 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 [
'email' => 'required'
];
}
// In case you need to customize the authorization response
// although it should give a general '403 Forbidden' error message
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof \Dingo\Api\Http\Request) {
throw new HttpException(403, 'You cannot access this resource'); //not a user?
}
}
}
PS: This is tested on Laravel 5.2.*
Hope it helps :)
According to the Wiki
you must overload the failedValidation and failedAuthorization methods.
These methods must throw one of the above mentioned exceptions and not the response HTTP exceptions that Laravel throws.
If you take a look at Dingo\Api\Http\FormRequest.php, you'll see:
class FormRequest extends IlluminateFormRequest
{
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
*
* #return mixed
*/
protected function failedValidation(Validator $validator)
{
if ($this->container['request'] instanceof Request) {
throw new ValidationHttpException($validator->errors());
}
parent::failedValidation($validator);
}
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof Request) {
throw new HttpException(403);
}
parent::failedAuthorization();
}
}
Hence, you need to change the names of your methods appropriately, and have them throw the appropriate exceptions, instead of returning a boolean.
you need to call the validate function explicitly when you run it under an Dingo API setup, try something like this (for L5.2):
Probably a few extra providers
...
Illuminate\Validation\ValidationServiceProvider::class,
Dingo\Api\Provider\LaravelServiceProvider::class,
...
Aliases
...
'Validator' => Illuminate\Support\Facades\Validator::class,
...
I'm also pretty much sure that you really don't want to use this below as suggested here and there, It will expect form(encoded) input and will also probably fail on CSRF token as it expects it, so it will fail right after validating (form input). But make sure to test behavior with this on/off.
use Dingo\Api\Http\FormRequest;
Make your headers:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Dingo\Api\Exception\ValidationHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/* This can be a tricky one, if you haven't split up your
dingo api from the http endpoint, there are plenty
of validators around in laravel package
*/
use Validator;
Then the actual code (if you adhere to cors standard,
this should be a POST and that commonly translates to a store request)
...
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function register(RegisterUserRequest $request) {
$validator = Validator::make($request->all(), $this->rules());
if ($validator->fails()) {
$reply = $validator->messages();
return response()->json($reply,428);
};
dd('OK!');
};
...
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
// or/and 'userid' => 'required'
];
}
That will give you back the response you expect from the validator. If you use this with pregenerated forms, it does not need this fix, there the validator will kick in automatically. (not under Dingo Api).
you probably also need these in composer.json
"dingo/api": "1.0.*#dev",
"barryvdh/laravel-cors": "^0.7.1",
This is untested, by heart, it took me 2 days to figure this out but I have a separate namespace for API specific and authenticated with middleware. success

Laravel 4. default controller route wont accept arguments

I have a RESTful controller for my users to handle the viewing of a users profile.
The problem is this:
I want the url to look like this www.example.com/user/1
This would show the user with the id of 1. The problem is that when i define the getIndex method in the UserController it wont accept the id as an argument.
Here is my routes.php portion:
Route::controller('user', 'UserController');
Now, it is my understanding that getIndex is sort of the default route if nothing else is supplied in the url, and so this:
public function getIndex() {
}
within the UserController will accept routes,
"www.example.com/user/index"
and
"www.example.com/user"
and it does!
However, if I include an argument that it should take from the url, it no longer works:
public function getIndex($id) {
//retrieve user info for user with $id
}
This will only respond to
"www.example.com/user/index/1"
and not
"www.example.com/user/1"
How can i make the latter work? I really do not want to clutter up the url with the word "index" if it is not necessary.
If you are planning to do this, the best way is to use RESTful controllers.
Change your route to this one,
Route::resource('user', 'UserController');
Then generate a controller using php artisan command,
php artisan controller:make UserController
This will generate your controller with all RESTful functions,
<?php
class UserController extends \BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index() // url - GET /user (see all users)
{
//
}
/**
* Show the form for creating a new resource.
*
* #return Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store() // url - POST /user (save new user)
{
//
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id) // url - GET /user/1 (edit the specific user)
{
//
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* #param int $id
* #return Response
*/
public function update($id) // url - PUT /user/1 (update specific user)
{
//
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return Response
*/
public function destroy($id) // url - DELETE /user/1 (delete specific user)
{
//
}
}
For more info, see this one Laravel RESTful controller parameters
To display www.example.com/user/1 on address bar you should use show method. In Laravel, restful controller by default create 7 routes. Show is one of them.
in your controller create a method like the following:
public function show($id)
{
// do something with id
$user = User::find($id);
dd($user);
}
Now, Browse http://example.com/user/1.

Categories