I'm trying to learn Laravel, but don't quite understand how i can accomplish my task the best way.
Scenario,
A user (user_id 5) may create a todo list, with the id of 3 (todo_id 3). Each of them live in their own model, so users model, and todo model. I'm currently checking if they created the todo by passing this into the method,
public function show($id)
{
TodoList::where('id',$id)->where('user_id', auth::id())->firstOrFail();
return 'ok';
}
So, i was thinking it would be possible to filter all this, using middleware. I have more models that requires the same action, so user 2, can't change/edit the todo_list for user 5 etc. And this needs to be attached to all methods, to block their access. Show, View, Delete etc.
Is there any way of doing this, so i don't have to create a new middleware for each model?
Laravel Eloquent provides Query Scopes for you, this might be what you're looking for.
Your Model:
class TodoList extends Model {
public function scopeFiltered($query, $id, $user_id)
{
return $query->where('id',$id)->where('user_id', $user_id);
}
}
Calling:
TodoList::filtered($id, Auth::user()->id)
->get();
Related
Here is my current situation:
I have a Task model.
Tasks have owners (a belongsTo relationship)
Owners have accounts (yet another belongsTo relationship)
I'd like to set up a "belongsToThrough" relationship from Tasks to Accounts.
My first solution was to define a relationship in the Tasks model, like this:
public function account(): BelongsTo
{
return $this->owner->account();
}
With it I could call $task->account and retrieve a task's account easily. The problem is that this doesn't work with load/with, which in turn causes problems because I can't refresh() a task that has had the account loaded in (because refresh uses load). The error just states Trying to call account() on null which was honestly expected.
My second solution was to change the relationship method to:
public function account(): BelongsTo
{
return $this->owner()->first()->account();
}
With this, I can also simply call $task->account and retrieve the model, and when loading, it doesn't work (returns null), but also doesn't throw any errors. I don't need to load this relationship in, it just happens that sometimes I need to refresh models and having the load method throw an error is not ok.
In summary
What I'm looking for is kind of a BelongsToThrough, as a Task would BelongTo an Account through an Owner (User). Is there a way to do this that works using both $task->account and $task->load('account'). Before you tell me I can load it using owner.account, I know that, but refresh() will do it automatically with load('account') so I need it to work like that, not with the dot notation.
To get it working with load(), you'll need to define an account relationship on the owner model, if you haven't done so already. Like this:
public function account() :BelongsTo
{
return $this->belongsTo(AccountsTable);
}
Then use dot notation when calling load() on your task model like:
$task->load('owner.account');
You can do that using eager loading
public function account()
{
return $this->belongsTo('App\ParentModel', 'foreignkey', 'localkey');
}
After that you can easily fetch relation data with load/with.
Thanks,
I have a Location Model, which contains two properties: ID and Name.
To edit this Model, I have set up this route:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
I set up very simple permissions: In the AuthServiceProvider I am checking in the boot method the following
Gate::before(function ($user, $permission) {
if ($user->permissions->pluck('name')->contains($permission)) {
return true;
}
});
Where permission is a Model that contains an ID and a name, mapped via a permission_user table.
I have these permissions set up:
edit_los_angeles
edit_new_york
edit_boston
plenty_of_other_permissions_not_related_to_location
After all this rambling, my actual question:
How can I tie these permissions to the edit the location?
The problem that I am facing is, that a given user is not allowed to edit all locations but may only be allowed to edit one location. Only the user with permission edit_los_angeles would be allowed to edit the Location with the name Los Angeles.
So I cannot group this into one permission like edit_location and add this to my route ->middleware('can:edit_location').
Instead, I would need something like this, I guess:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_los_angeles');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_new_york');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_boston');
...obviously this would not work.
What would be your approach to tackle this dilemma? :-)
Maybe I am doing something completely wrong and there is a better Laravel-Way of doing this?
Thank you very much for your help in advance!
I am using Laravel 6.0 :-)
Two assumption for my approach to work, use model binding in the controller (you should do that no matter what). Secondly there needs to be a relation between location and the permission it needs, something similar to the slug you suggested.
Your controller function would look something like this. Adding a FormRequest is a good approach for doing this logic.
class LocationController {
public function edit(EditLocationRequest $request, Location $location) { // implicit model binding
...
}
}
For ease of use, i would also make a policy.
class LocationPolicy
{
public function edit(User $user, Location $location) {
return $user->permissions->pluck('name')
->contains($location->permission_slug); // assuming we have a binding
}
}
Remember to register policy in the AuthServiceProvider.php.
protected $policies = [
Location::class => LocationPolicy::class,
];
Now in your form request consume the policy in the authorize method. From here you are in a request context, you can access user on $this->user() and you can access all models that are model binding on their name for example $this->location.
class EditLocationRequest
{
public function authorize(): bool
{
return $this->user()->can('edit', $this->location);
}
}
Now you should be able to only have a single route definition.
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
EDIT
Withouth the form request if you use the trait AuthorizesRequests you can do the following. This will throw an AuthorizationException of it fails.
use AuthorizesRequests;
public function edit() {
$this->authorize('edit', $location);
}
If you have a requirement based upon the location relationship, then you will need to capture that relationship in the data. A good starting point to this would be to add a pivot table specific for these editing permissions. Consider a table, location_permissions, with a user_id and a location_id. You could then modify or add permission middleware to do a check for a record in this table once you have a specific user and location.
Edit: to answer the question about implementation of middleware,
The crux of the implementation would likely be solved by defining a relationship on the user model to location via this new pivot table.
I would recommend then adding an additional method which consumes the new locations relationship to the model along the lines of
public function canEditLocation(Location $location): bool {
return $this->locations
->where('location_id', '=', $location->id)
->count() > 0;
}
And the actual middleware something along these lines:
public function handle($request, Closure $next, $location)
{
if (! $request->user()->canEditLocation($location)) {
\\handle failed permission as appropriate here.
}
return $next($request);
}
My middleware parameters knowledge is rusty, but I believe that is correct as defined at https://laravel.com/docs/master/middleware#middleware-parameters
i'm using Laravel as my PHP framework. its a convention to put index show store ... functions in controllers.
i have 2 types of users(Admin & normal user). lets assume there is an Order(in restaurant) model and i want to implement index function for its controller.
a user can have more than one Order.
what i need is that this function:
if admin is calling this API: returns all Orders
if normal user is calling this API: returns just Orders owned by this user
i searched but i couldn't find anything(tbh i didn't know what to search).
once i did this as below which i didn't like because it looks two different functions gathered in one:
if ($user->role == admin) {
// fetch all orders
} else if ($user->role == normal_user) {
// just find user orders
}
so my question is what's best approach to achieve what i want?
Such a REST API endpoint is typically a search allowing multiple filters, sorting and pagination. If so it is completly fine to apply different defaults for filters and also restrict filters to roles.
I would auto apply a filter user=currentUser for missing admin role and return a forbidden if a user without the admin role tries to apply a user filter for a different user.
With this approach you give admins also the functionality to search for offers of a specific user and you only need one search api to be used by the controller.
Why don't use an if statement?
You could make a scope on the model but then you'll still have an if.
What about this?
if ($user->role == admin) {
Order::all();
} else if ($user->role == normal_user) {
$user->orders()->get();
}
Or make it an inline if
$user->role == admin ? Order::all() : $user->orders()->get();
IMO the best practice here is to make a different Admin/OrderController.php
Then with middleware check wat, the role of the user is, and then redirect them to the admin controllers.
Since you'll probably also want an update and delete, or other functions only accesible by an Admin
I had a similar question myself a while ago and ended up with this strange solution to avoid that if/else block.
Assumptions
I assumed the existence of an helper method in the User model called isNot($role) to verify the if the user's role matches or not the given one.
This is just an example to give the idea of the check, but you should implement the condition as you like.
Second assumption I made is that each order has a user_id field which will reference the owner of that order though his id (FK of 1:N among user and order).
Implementation
public function index(Request $request)
{
$orders = Order::query()
->when($request->user()->isNot('admin'), function ($query) use ($request) {
return $request->user()->orders();
// Or return $query->where('user_id', $request->user()->id);
})
->paginate();
return OrderResource::collection($orders);
}
The when method is the key here. Basically you call it like: when($value, $callback) and if $value is false the callback won't be executed, otherwise it will.
So for example, if the user is not an admin, you will end up executing this query:
Order::paginate();
that would fetch all the order with pagination (note that you could swap paginate with get.
Otherwise, the callback is gonna be executed and you will execute the paginate method on $request->user()->orders(); (orders called like a method is still a query builder object, so you can call paginate on it).
The query would be:
$request->user()->orders()->paginate();
If you instead opted for the second solution in the callback you would basically add a where condition (filtering on the user_id of the orders) to the main scope to get only the user's orders.
The query would be:
Order::query()->where('user_id', $request->user()->id)->paginate();
Finally, to better control what's sent back as response I use Laravel's API Resource (and I really suggest you to do so as well if you need to customize the responses).
NOTE: The code might have syntax and/or logical errors as it was just an on the fly edit from production code, and it hasn't been tested, but it should give an overall idea for a correct implementation.
it would be better to include the if/else in your order modal like this:
class Order extends Model {
....
static function fetchFor (User $user) : Collection
{
return $user->isAdmin() ? self::all() : self::where("user_id",$user->id);
}
}
then you can call this method on your controller
public function index()
{
return view('your-view')->with('orders',Order::fetchFor(Auth::user())->get())
}
You can create scope in Order class...
For example you have field user_id in Order, for detect user
class Order
{
...
public function scopeByRole($query)
{
if (!Auth::user()->isAdmin())
$query = $query->where('user_id', Auth::user()->id);
return $query;
}
}
in you controller just get all Orders with scope:
$orders = Order::byRole()->get();
it return you orders by you role
Also you need have in class User function for detect role, example
class User
{
public function isAdmin()
{
// you logic which return true or false
}
}
I would like to create a Laravel Authorisation Policy, however rather than checking the user->id I would like to check the related users Business model (like $user->business()->id)
I've tried using the following in my OrderPolicy but it does not work.
OrderPolicy
class OrderPolicy
{
....
public function edit(User $user, Order $order)
{
if ($user->business()->id === $order->business_id) {
return true;
}
}
}
Blade
...
#can('edit', $business->orders())
Edit Link
#endcan
...
Could someone show me how I could do this correctly?
Assuming business() is a relationship method.
$user->business->id would be the id of the Business model that is related to the user.
May want to check that ->business isn't null first.
You can also query directly on the relationship if you don't want to load that relationship. $user->business()->where('id', $order->business_id)->exists()
Laravel 5.4 Docs - Eloquent - Relationships - Relationship Methods vs Dynamic Properties
Here is model structure of my Laravel 5.3 project,
User.php (Model)
it has one invitation method that returns the invitation of a user.
public function invitations()
{
return $this->hasMany( 'App\Invitation', 'invitee_id', 'id' );
}
Invitation.php (Model)
This model has another method that would return the inviter detail of an invitation.
public function inviter()
{
return $this->hasOne( 'App\User', 'id', 'invited_by' );
}
If i want to retrieve all invitations of current user it works,
\Auth::user()->invitations;
But if i try to get the information about the inviter it won't work! (Question: How to do it?)
\Auth::user()->invitations->inviter;
Though i can query the inviter from a invitation eloquent object like this,
\App\Invitation::first()->inviter;
But this is not working when i try to access it from the user model -> invitation -> inviter!
Also can i use eager loading here?
\Auth::user()->invitations->inviter;
Looking at this, it appears that you're attempting to retrieve the inviter property from a collection of invitations. The reason Ken's suggestion to use \App\Invitation::first()->inviter; worked is because you are retrieving the inviter of only one invitation (in this instance, the first). To resolve this, loop through your invites before attempting to retrieve the properties for each one:
$invitations = \Auth::user()->invitations;
foreach ($invitations as $invitation) {
$inviter = $invitation->inviter;
}
There is also an each() method specific to Laravel Collections that will allow you to loop through your object.