I am not sure if I am using this correctly, but I am utilising the requests in Laravel 5, to check if the user is logged in and if he is the owner of an object. To do this I need to get the actual object in the request class, but then I need to get the same object in the controller?
So instead of fetching it twice, I thought, why not just set the object as a variable on the request class, making it accessible to the controller?
It works, but I feel dirty? Is there a more appropriate way to handle this?
Ex.
Request Class
class DeleteCommentRequest extends Request {
var $comment = null;
public function authorize() {
$this->comment = comment::find(Input::get('comment_id'));
$user = Auth::user();
if($this->comment->user == $user)
return true;
return false;
}
public function rules() {
return [
'comment_id' => 'required|exists:recipes_comments,id'
];
}
}
Ex. Controller:
public function postDeleteComment(DeleteCommentRequest $request) {
$comment = $request->comment;
$comment->delete();
return $comment;
}
So what is my question? How do I best handle having to use the object twice when using the new Laravel 5 requests? Am I possibly overextending the functionality of the application? Is it ok to store the object in the application class so I can reach it later in my controller?
I would require ownership on the query itself and then check if the collection is empty.
class DeleteCommentRequest extends Request {
var $comment = null;
public function authorize() {
$this->comment = comment::where('id',Input::get('comment_id'))->where('user_id',Auth::id())->first();
if($this->comment->is_empty())
return false;
return true;
}
public function rules() {
return [
'comment_id' => 'required|exists:recipes_comments,id'
];
}
}
Since you're wanting to use the Model in two different places, but only query it once I would recommenced you use route-model binding.
In your RouteServiceProvider class (or any relevant provider) you'll want to bind the comment query from inside the boot method. The first parameter of bind() will be value that matches the wildcard in your route.
public function boot()
{
app()->router->bind( 'comment_id', function ($comment_id) {
return comment::where('id',$comment_id)->where('user_id',Auth::id())->first();
} );
}
Once that's set up you can access the Model from your DeleteCommentRequest like so
$this->comment_id
Note: The variable is Comment_id because that's what matches your route, but it will contain the actual model.
From your controller you just inject it like so
public function postDeleteComment(Comment $comment, DeleteCommentRequest $request) {
$comment->delete();
return $comment;
}
Related
I am creating a new API call for our project.
We have a table with different locales. Ex:
ID Code
1 fr_CA
2 en_CA
However, when we are calling the API to create Invoices, we do not want to send the id but the code.
Here's a sample of the object we are sending:
{
"locale_code": "fr_CA",
"billing_first_name": "David",
"billing_last_name": "Etc"
}
In our controller, we are modifying the locale_code to locale_id using a function with an extension of FormRequest:
// This function is our method in the controller
public function createInvoice(InvoiceCreateRequest $request)
{
$validated = $request->convertLocaleCodeToLocaleId()->validated();
}
// this function is part of ApiRequest which extend FormRequest
// InvoiceCreateRequest extend ApiRequest
// So it goes FormRequest -> ApiRequest -> InvoiceCreateRequest
public function convertLocaleCodeToLocaleId()
{
if(!$this->has('locale_code'))
return $this;
$localeCode = $this->input('locale_code');
if(empty($localeCode))
return $this['locale_id'] = NULL;
$locale = Locale::where(Locale::REFERENCE_COLUMN, $localeCode)->firstOrFail();
$this['locale_id'] = $locale['locale_id'];
return $this;
}
If we do a dump of $this->input('locale_id') inside the function, it return the proper ID (1). However, when it goes through validated();, it doesn't return locale_id even if it's part of the rules:
public function rules()
{
return [
'locale_id' => 'sometimes'
];
}
I also tried the function merge, add, set, etc and nothing work.
Any ideas?
The FormRequest will run before it ever gets to the controller. So trying to do this in the controller is not going to work.
The way you can do this is to use the prepareForValidation() method in the FormRequest class.
// InvoiceCreateRequest
protected function prepareForValidation()
{
// logic here
$this->merge([
'locale_id' => $localeId,
]);
}
I am having an issue setting up an injection on both the constructor and the method in a controller.
What I need to achieve is to be able to set up a global controller variable without injecting the same on the controller method.
From below route;
Route::group(['prefix' => 'test/{five}'], function(){
Route::get('/index/{admin}', 'TestController#index');
});
I want the five to be received by the constructor while the admin to be available to the method.
Below is my controller;
class TestController extends Controller
{
private $five;
public function __construct(PrimaryFive $five, Request $request)
{
$this->five = $five;
}
public function index(Admin $admin, Request $request)
{
dd($request->segments(), $admin);
return 'We are here: ';
}
...
When I run the above, which I'm looking into using, I get an error on the index method:
Symfony\Component\Debug\Exception\FatalThrowableError thrown with message "Argument 1 passed to App\Http\Controllers\TestController::index() must be an instance of App\Models\Admin, string given"
Below works, but I don't need the PrimaryFive injection at the method.
class TestController extends Controller
{
private $five;
public function __construct(PrimaryFive $five, Request $request)
{
$this->five = $five;
}
public function index(PrimaryFive $five, Admin $admin, Request $request)
{
dd($request->segments(), $five, $admin);
return 'We are here: ';
}
...
Is there a way I can set the constructor injection with a model (which works) and set the method injection as well without having to inject the model set in the constructor?
One way you could do this is to use controller middleware:
public function __construct()
{
$this->middleware(function (Request $request, $next) {
$this->five = PrimaryFive::findOrFail($request->route('five'));
$request->route()->forgetParameter('five');
return $next($request);
});
}
The above is assuming that PrimaryFive is an Eloquent model.
This will mean that $this->five is set for the controller, however, since we're using forgetParameter() it will no longer be passed to your controller methods.
If you've specific used Route::model() or Route::bind() to resolve your five segment then you can retrieve the instance straight from $request->route('five') i.e.:
$this->five = $request->route('five');
The error is because of you cannot pass a model through the route. it should be somethiing like /index/abc or /index/123.
you can use your index function as below
public function index($admin,Request $request){}
This will surely help you.
Route::group(['prefix' => 'test/{five}'], function () {
Route::get('/index/{admin}', function ($five, $admin) {
$app = app();
$ctr = $app->make('\App\Http\Controllers\TestController');
return $ctr->callAction("index", [$admin]);
});
});
Another way to call controller from the route. You can control what do you want to pass from route to controller
Is it better to create separate request class for each new method in controller or edit existing request class in laravel or any better idea ?
example
class fooBarController {
public function a(fooBarARequest $r) {
}
public function b(fooBarBrequest $r) {
}
public function c(fooBarCDRequest $r) {
}
public function d(fooBarCDRequest $r) {
}
}
Using extra request classes allows you to define validation rules which your request is checked against before it reaches your controller. You can also handle authorization in the request class. An example would be:
class UpdateAccountEmail extends FormRequest
{
public function authorize()
{
return true; // authorization is handled on route/middleware level
}
public function rules()
{
return [
'new_email' => 'required|email|confirmed',
'new_email_confirmation' => 'required',
];
}
}
So, to sum it up: it does not make sense to use a custom request class for requests which do not have payload that needs to be validated. This means, for a normal GET request we most likely (of course there are exceptions) want to use the normal Request class provided by laravel. A controller like this would be quite normal:
class AccountController
{
public function show(Request $request)
{
return view('account.show', ['user' => $request->user()]);
}
public function edit()
{
return view('account.edit', ['user' => \Auth::user()]);
}
public function updateEmail(UpdateAccountEmail $request)
{
$user = $request->user();
$user->email = $request->input('new_email');
$user->save();
return redirect()->route('account.index');
}
public function logins(Request $request)
{
$logins = $request->user()->logins()
->when($request->get('filter_from'), function ($query, $from) {
$query->where('created_at', '>=', $from);
})
->when($request->get('filter_until'), function ($query, $until) {
$query->where('created_at', '<=', $until);
})
->get();
return view('account.logins', ['logins' => $logins]);
}
}
As you can see, for the GET request that is handled by logins(Request $request), we do not use a custom request class because we don't need to validate anything (well, we could validate the filter parameters, but for simplicity we don't).
The example above also shows different methods of retrieving the current user. Even for that you don't need a request at all.
This is no actual production code, just something off the top of my head...
In my user controller I have an posts function, which gives access to a sub-resource of users. This is accessed through the /users/{id}/posts endpoint.
I want the pass the $id from the request URL into a UserPolicy method:
public function resource($user, $id)
{
return $user->id === $id;
}
My UserController method:
public function posts(Request $request, $id)
{
$this->authorize('resource', $id);
return response()->json(['events' => []], 200);
}
Is there anyway to do this? I notice that Policy methods seem to ignore anything that isn't an object.
Edit:
I am currently using a helper method for this authorization but would like to move it to my Policy to keep all rules together:
public function authorizeResource($id)
{
if ((int)$id !== (int)$this->auth->user()->id) {
throw new \Exception;
}
}
Laravel needs to know which policy class to use. For that you need to specify the model, in this case passing an array with an instance of user first and then the $id. Laravel uses the spread operator and will inject the $id as a parameter on your callback function.
//UserController.php
public function posts(Request $request, $id)
{
$this->authorize('resource', [User::class, $id]);
return response()->json(['events' => []], 200);
}
I'm trying to allow user to view their own profile in Laravel 5.4.
UserPolicy.php
public function view(User $authUser, $user)
{
return true;
}
registered policy in AuthServiceProvider.php
protected $policies = [
App\Task::class => App\Policies\TaskPolicy::class,
App\User::class => App\Policies\UserPolicy::class
];
Routes
Route::group(['middleware' => 'auth'], function() {
Route::resource('user', 'UserController');
} );
Blade template
#can ( 'view', $user )
// yes
#else
// no
#endcan
UserController.php
public function profile()
{
return $this->show(Auth::user()->id);
}
public function show($id)
{
$user = User::find($id);
return view('user.show', array( 'user'=>$user,'data'=>$this->data ) );
}
The return is always 'false'. Same for calling policy form the controller. Where do I go wrong?
Answering my own question feels weird, but I hate it when I come across questions without followups.
So after double checking It turned out that if I remove authorizeResource from the constructor:
public function __construct()
{
$this->authorizeResource(User::class);
}
and check for authorization in the controller function:
$this->authorize('view',$user);
everything works.
I must've missed this part when I added $user as a parameter in the policy function. So the user to be viewed is never passed in the authorizeResource method.
Thanks everyone for taking your time to help me.
When you add
public function __construct()
{
$this->authorizeResource(User::class);
}
to your Controller, you have to edit all your function signatures to match it to the class e.g. your show signature has to change from public function show($id)
to public function show(User $user)
After that it should work
Just a different approach here to users viewing their own profile.
First, I will create a route for that
Route::group(['middleware' => 'auth'], function() {
Route::get('profile', 'UserController#profile');
});
Then in the profile function I do
public function profile()
{
$user = Auth::user();
return view('profile', compact('user'));
}
This way, user automatically only views their own profile.
Now, if you want to allow some users to view others' profiles, then you can use Policy. Why? Because I think user should ALWAYS be able to view their own profile. But not all users should view other users profiles.
Solution:
Change the second parameter from #can( 'view', $user ) to #can( 'view', $subject ) and it will work find.
Why:
Because you're doing it the wrong way.
public function view(User $user, $subject){
return true;
}
Just look carefully the policy view method, first parameter is authenticated user or current user and second parameter is $subject, Since policies organize authorization logic around models.
Policies are classes that organize authorization logic around a
particular model or resource. For example, if your application is a
blog, you may have a Post model and a corresponding PostPolicy to
authorize user actions such as creating or updating posts.
if you want to go further deep inside it.
https://github.com/laravel/framework/blob/5.3/src/Illuminate/Auth/Access/Gate.php#L353
/**
* Resolve the callback for a policy check.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $ability
* #param array $arguments
* #return callable
*/
protected function resolvePolicyCallback($user, $ability, array $arguments)
{
return function () use ($user, $ability, $arguments) {
$instance = $this->getPolicyFor($arguments[0]);
// If we receive a non-null result from the before method, we will return it
// as the final result. This will allow developers to override the checks
// in the policy to return a result for all rules defined in the class.
if (method_exists($instance, 'before')) {
if (! is_null($result = $instance->before($user, $ability, ...$arguments))) {
return $result;
}
}
if (strpos($ability, '-') !== false) {
$ability = Str::camel($ability);
}
// If the first argument is a string, that means they are passing a class name
// to the policy. We will remove the first argument from this argument list
// because the policy already knows what type of models it can authorize.
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
if (! is_callable([$instance, $ability])) {
return false;
}
return $instance->{$ability}($user, ...$arguments);
};
}
See the last line where it is calling the method with $user and $argument( in our case Model ) is passed.
Laravel Docs for Authorization/Policies
It's possible to escape one or more policies methods using options parameter at authorizeResource with except:
public function __construct()
{
$this->authorizeResource(User::class, 'user', ['except' => ['view']]);
}
This should be on Laravel's documentation, but it isn't. I discovered this just guessing. I think this way it is a better approach thus, by removing authorizeResource method in the construct, it would be necessary to implement the authorization method for each resource action in order to protect the controller.