Laravel stop modal attribute being updated based on user role - php

I'm creating a system in which there are users, and user have many user roles. The user roles also contain permissions. Some fields are protected, so that they cannot be overwritten without the user possessing a specific permission.
For example, a user may have the attribute "email" which cannot be changed by the user, unless the user has the permission "update-email-address".
I originally intended to implement this concept as a trait or an abstract class, but I can't figure a way of doing this which doesn't involve either overloading the Eloquent Model constructor method, or else completely overloading another method.
What I'm hoping to do, is to be able to specify an array in a model like below, and by using a tract or extention, somehow prevent updating a model attribute:
/**
* The attributes that should only be updatable by given user with the
* specified permission
*
*/
public $update_only_by_permission = [
'email' => ['update-email-address'],
];
Is there a way to achieve this?

I stumbled across a way to provide a boot method for a trait extending a model, and was able to achieve this through the following:
Trait used on many Eloquent Models:
use Auth;
trait AttributeVisibleByPermissionTrait {
/**
* Stops updating of the object attributes if the authenticated
* user does not have the given permission provided in the
* $object->update_only_by_permission array.
*/
public static function bootAttributeVisibleByPermissionTrait(){
static::updating(function($object){
foreach ($object->update_only_by_permission as $attribute => $permissions) {
foreach ($permissions as $permission) {
if(!Auth::User()->can($permission)) {
unset($object->$attribute);
}
}
}
});
}
}
User Model:
class User extends Authenticatable
{
use AttributeVisibleByPermissionTrait;
/**
* The attributes that should only be updated by given user auth permissions
*
* #var array
*/
public $update_only_by_permission = [
'email' => ['update-email-address'],
];
}

Related

Stopping 'with' attribute recursion in Laravel Eloquent model

Assume two Eloquent models in Laravel - user and location.
User:
class User extends Model
{
/**
* Relations that should be automatically loaded.
*
* #var array
*/
public $with = [
'locations'
];
/**
* Returns relation to the locations table.
*/
public function locations() {
return $this->belongsTo('App\Entities\Location');
}
}
Location:
class Location extends Model
{
/**
* Relations that should be automatically loaded.
*
* #var array
*/
public $with = [
'users'
];
/**
* Returns relation to the users table.
*/
public function users() {
return $this->hasMany('App\User');
}
}
As you can see, they both have the public attribute with - because as these models are driving an API, I want the users to always be returned with their location, and locations to always be returned with a list of their users.
However, as I thought might happen, this has led to the request timing out due to (I think) recursion. In other words, the location loads its users, and they load their locations, which then load their users, and so on.
Is there any way in Laravel to force it not to recurse into the same model that is doing the 'initial loading'?
I should mention that user also has another relation, profile, that I want to load in every query. So if, for example, there's a way to simply kill with eager-loading after a certain 'depth', that won't do it.
EDIT: I am aware that I can use User::with('location')->find(); to eager-load the relations, however this solution requires me to use that extra code everywhere I am retrieving users. I am looking for a solution that will let me always eager-load the relationships automatically.
It makes a infinite loop of loading for example you called a User now it is loading Location but when Location loaded then it again called User it has been loaded and the same loading process is going on so it makes a loop which is not even breaking. that why it is give a error
you can do like this
User::with('location')->find(..) // or get or any
You can resolve this problem, just put other model in belongsTo method.
Create new empty JustLocation model extends Location model.
Set protected $with = []; in JustLocation model
Change return $this->belongsTo('App\Entities\Location'); to return $this->belongsTo('App\Entities\JustLocation'); in the User model.

How can I append an Eloquent property from a related model without getting the model's contents?

I have 2 eloquent models:
EloquentUser
and
SharedEvents
They are both related by user_id
I'm attempting to set up and appends attribute in the SharedEvents model that will append the full_name of the user with whom the event has been shared.
For the sake of readability, I'm only including the appends components of my class
class SharedEvents extends Model {
protected $appends = ['fullName'];
/**
* #return BelongsTo
*/
public function user() : BelongsTo {
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id');
}
/**
* #return mixed
*/
public function getFullNameAttribute(){
return $this->user->user_full_name;
}
Unfortunately when I run this I'm getting back both the full name and the entire user model when I only want the full name.
Is there a way to avoid attaching the content of the user model?
It feels like you're trying to make columns from your EloquentUser model first class citizens in your SharedEvent model. You're getting close, but consider...
When working with relationships, this is a good way to be explicit:
Assuming user_full_name is an accessor on your User model:
// EloquentUser.php
// This will automatically add your accessor to every query
// as long as you select the columns the accessor is made
// up of
protected $appends = ['user_full_name'];
/**
* User Full Name Accessor
* #return string
*/
public function getUserFullNameAttribute()
{
return $this->first_name . ' ' . $this->last_name;
}
// SharedEvent.php
/**
* A SharedEvent belongs to an Eloquent User
* #return BelongsTo
*/
public function user() : BelongsTo
{
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id');
}
// Somewhere in your controller or wherever you want to access the data
$sharedEvents = SharedEvent::with(['user' => function ($query) {
$query->select('user_id', 'first_name', 'last_name');
}])->where(...)->get(['shared_with_id', ...other columns]); // you must select the related column here
This should get you the closest to what you want, but there are a couple of things you should know:
If user_full_name is an accessor, you need to select all of the columns that make up that accessor (as I mention above)
You must select the related keys (user_id in EloquentUser and shared_with_id in SharedEvent)
The $appends is necessary in EloquentUser here because you can't directly add an accessor to your sub query inside the closure.
Try to get comfortable with using a closure as the 2nd argument in your relationships. It's the best way to really be precise as to which columns you're selecting when you're loading relationships — Eloquent makes it really easy to be lazy and just do:
SharedEvent::with('user')->get();
which as you've see will just do a select * on both SharedEvent and your user relationship.
Another thing I've noticed when working with complex queries that use relationships is that you can quickly reach a point where it feels like you're fighting the framework. That's often a sign to consider simplifying ot just using raw SQL. Eloquent is powerful, but is just another tool in your programming tool belt. Remember that you have other tools at your disposal.
I was running into the same problem, and my first idea was to explicitly hide the (entire) associated model when fetching SharedEvent.
In your case, this would look like:
class SharedEvents extends Model {
protected $appends = ['fullName'];
protected $hidden = ['user'];
...
}
I actually decided against this since I am returning a lot of other attached models, so I decided on attaching the entire user, but I tested it and it works.
FYI, I'm not sure if the discrepancy is due to an older version of Laravel, but Eloquent actually expects $appends attributes to be in snake-case (https://laravel.com/docs/5.6/eloquent-serialization#appending-values-to-json):
Note that attribute names are typically referenced in "snake case", even though the accessor is defined using "camel case"
Can you try this?
public function user() : BelongsTo {
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id')->select('fullName','user_id');
}
After doing some research and experimentation, it doesn't look like there's a way to do this with Eloquent. The solution I ended up going with unfortunately is running an additional query.
/**
* This is inefficient, but I could not find a way to get the full name
* attribute without including all of user.
*
* #return string
*/
public function getFullNameAttribute(){
return EloquentUser::select('user_full_name')
->where('user_id', $this->shared_with_id)
->first()
->user_full_name;
}

As correctly use policies in laravel 5.2? I can not

I have a problem, I can not use policies in laravel 5.2.
I have 2 tables, students and tasks.
I try to apply a policy to prevent editing of a task by changing the url, but I always get the message This action is unauthorized although the task is the correct user.
Policy Code:
<?php
namespace App\Policies;
use App\Models\Student;
use App\Models\Task;
class TasksPolicy
{
public function edit(Student $student, Task $tasks)
{
return $student->id === $tasks->student_id;
}
}
Code in AuthServiceProvider.php
<?php
namespace App\Providers;
use App\Models\Task;
use App\Policies\TasksPolicy;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
Task::class => TasksPolicy::class
];
And then the call in the TaskController.php file:
public function edit($id)
{
$tasks = Task::findOrFail($id);
$this->authorize('edit', $tasks);
return view('tasks.edit', compact('tasks'));
}
I think the code is good because I've revised several times, but as I said earlier I always get the message This action is unauthorized although the task is to edit the user.
http://i.imgur.com/2q6WFb3.jpg
What am I doing wrong? As I can use the policy correctly?
you are using "===" which means that both side data and datatype will match.May be your data are matched,not datatype,you may try using "=="
public function edit(Student $student, Task $tasks)
{
return $student->id == $tasks->student_id;
}
Two things: one is the name of the method and the other is the order of parameters. The method name should be 'update', not 'edit' - these are predefined, at least in later versions of Laravel. You might be getting the authorization error because the name 'edit' is not recognized by Laravel, so the policy for update is never defined.
The order of arguments also matters. When there are parameters passed to policy methods, the User model has to be the first parameter, followed by all the others.
public function update(User $user, [... other objects...])
So, you'd have
update(User $user, Student $student, Task $tasks)
Laravel will inject the Authenticated User Model but other objects have to be passed directly.
$this->authorize('edit', $student, $tasks);
Hopefully that will work.
If your Student class extends User Class, you may be thinking that you can substitute Student for User in the method prototype. You can't do that - that's a different method altogether.

Proper way of MVC pattern when passing data to View?

I'm currently reading up on proper MVC, and I'm refactoring some code of my current project. It's written in Laravel. I'll have simplified the problem to this:
I have a domain object 'User' and 'Log'. A User has many Logs.
User domain object:
class User extends Authenticatable implements UserInterface
{
protected $fillable = ['surname', 'name','email'];
protected $hidden = [
'password', 'remember_token',
];
public function logs()
{
return $this->hasMany('App\Models\Log');
}
}
Log domain object:
class Log extends Model implements LogInterface
{
public $timestamps = false;
protected $fillable = [
'log_date',
'log_hours',
'log_minutes'
];
protected $dates = ['log_date'];
public function user()
{
return $this->belongsTo('App\Models\User');
}
}
In my view, I want to display the total hours logged of a certain day. I'm currently injecting a class in my controller which has a method calcHoursPerDay($user_id) which injects the LogRepository to get the Logs of that user on a given date and return the calculated value to the view.
Reading up on proper MVC, I'm inclined to drop the class and create a method for my User domain object called HoursOnDay($date). So keeping the bussiness logic in the Model.
Is this the way of using MVC, and if not, what would the proper approach?
Yes, that's the proper way to do things in Laravel and MVC. calcHoursPerDay() should be part of User or Log model. Also, look at scopes doc, I guess that's what you want to use:
https://laravel.com/docs/5.1/eloquent#query-scopes

Laravel Policy not injecting current user

I'm trying to bind a policy for a user that checks if he has a record in another table and a flag set to true. However, the policy I'm using doesn't inject the current user.
AuthServiceProvider.php
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
\App\Http\Models\User::class => \App\Policies\UserPolicy::class,
];
App\Policies\UserPolicy
<?php
namespace App\Policies;
use App\Http\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
public function view(User $user)
{
return true;
// currently just debug value
}
}
In the laravel docs it's stated as:
All policies are resolved via the Laravel service container, meaning you may type-hint any needed dependencies in the policy's constructor and they will be automatically injected.
When using auth()->user()->can('view') - in a controller that depends on the records of the table I'll check - always returns false, no matter what. The only way to get it working is by calling
auth()->user()->can('view', auth()->user()) or $this->authorize('view', auth()->user())
What am I missing to get the first method to work?
EDIT: It doesn't matter if I typehint the user in the method itself or in the constructor as it seems to be ignored.

Categories