Skip to main content

Black Friday 2025! Only until December 1st: coupon FRIDAY25 for 40% off Yearly/Lifetime membership!

Read more here

Tasks CRUD: Permissions vs Global Scopes

Premium
4:07

Finally, we get to the actual point of this small application: Task management.

Compared to the Task Model in previous lessons of this course, we added a few more fields: assigned_to_user_id (clinic doctor/staff) and patient_id:

Tasks Migration:

$table->foreignId('assigned_to_user_id')->constrained('users');
$table->foreignId('patient_id')->constrained('users');

Then, I added them to the Model, too:

app/Models/Task.php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;
 
class Task extends Model
{
use HasFactory;
 
protected $fillable = [
'name',
'due_date',
'assigned_to_user_id',
'patient_id',
'team_id',
];
 
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_to_user_id');
}
 
public function patient(): BelongsTo
{
return $this->belongsTo(User::class, 'patient_id');
}
}

Then, we also changed the Factory with the new columns in mind.

database/factories/TaskFactory.php

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
 
class TaskFactory extends Factory
{
public function definition(): array
{
$randomAssignee = collect([
User::factory()->doctor(),
User::factory()->staff(),
])->random();
 
return [
'name' => fake()->text(30),
'due_date' => now()->addDays(rand(1, 100)),
'assigned_to_user_id' => $randomAssignee,
'patient_id' => User::factory()->patient(),
];
}
}

Now, who can manage tasks? Traditionally, let's start with Policy:

app/Policies/TaskPolicy.php

use App\Enums\Role;
use App\Models\Task;
use App\Models\User;
use App\Enums\Permission;
 
class TaskPolicy
{
public function viewAny(User $user): bool
{
return $user->hasPermissionTo(Permission::LIST_TASK);
}
 
public function create(User $user): bool
{
return $user->hasPermissionTo(Permission::CREATE_TASK);
}
 
public function update(User $user, Task $task): bool
{
return $user->hasPermissionTo(Permission::EDIT_TASK);
}
 
public function delete(User $user, Task $task): bool
{
return $user->hasPermissionTo(Permission::DELETE_TASK);
}
}

You don't see the filter by team here, right? The approach we took here is to filter them on the Eloquent level, with global scope.

In fact, it's a 2-in-1 scope...

The Full Lesson is Only for Premium Members

Want to access all of our courses? (31 h 16 min)

You also get:

55 courses
Premium tutorials
Access to repositories
Private Discord
Get Premium for $129/year or $29/month

Already a member? Login here

Comments & Discussion

J
joncrzo ✓ Link copied!

When I run the test command on ssh, I keep getting the same errors.

user18@server1:~/public_html$ php artisan test --filter=TaskTest

PASS Tests\Feature\TaskTest ✓ it allows clinic admin and staff to access create task page with (Closure Object ()) #1 0.46s
✓ it allows clinic admin and staff to access create task page with (Closure Object ()) #2 0.06s
✓ it allows clinic admin and staff to access create task page with (Closure Object ()) #3 0.07s
✓ it does not allow patient to access create task page 0.06s
✓ it allows clinic admin and staff to enter update page for any task in their team with (Closure Object ()) #1 0.09s
✓ it allows clinic admin and staff to enter update page for any task in their team with (Closure Object ()) #2 0.10s
✓ it allows clinic admin and staff to enter update page for any task in their team with (Closure Object ()) #3 0.10s
✓ it does not allow administrator and manager to enter update page for other teams task with (Closure Object ()) #1 0.06s
✓ it does not allow administrator and manager to enter update page for other teams task with (Closure Object ()) #2 0.06s
✓ it does not allow administrator and manager to enter update page for other teams task with (Closure Object ()) #3 0.06s
✓ it allows administrator and manager to update any task in their team with (Closure Object ()) #1 0.07s
✓ it allows administrator and manager to update any task in their team with (Closure Object ()) #2 0.07s
✓ it allows administrator and manager to update any task in their team with (Closure Object ()) #3 0.07s
✓ it allows clinic admin and staff to delete task for his team with (Closure Object ()) #1 0.06s
✓ it allows clinic admin and staff to delete task for his team with (Closure Object ()) #2 0.08s
✓ it does not allow doctor to delete tasks 0.06s
✓ it does not allow super admin and admin to delete task for other team with (Closure Object ()) #1 0.05s
✓ it does not allow super admin and admin to delete task for other team with (Closure Object ()) #2 0.05s
✓ it does not allow super admin and admin to delete task for other team with (Closure Object ()) #3 0.06s
✓ it shows users with a role of doctor and staff as assignees 0.07s
✓ it shows users with a role of patient as patients 0.09s
✓ it shows only teams tasks for doctor, staff, and clinic admin with (Closure Object ()) #1 0.08s
✓ it shows only teams tasks for doctor, staff, and clinic admin with (Closure Object ()) #2 0.06s
✓ it shows only teams tasks for doctor, staff, and clinic admin with (Closure Object ()) #3 0.06s
✓ it shows patient only his tasks 0.06s

FAIL Tests\Feature\UserTaskTest ⨯ it allows users to access tasks page 0.06s
⨯ it does not allow users to access admin task page 0.04s
⨯ it allows administrator to access tasks page 0.04s
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\UserTaskTest > it allows users to access tasks page RouteNotFoundException
Route [user.tasks.index] not defined.

at vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php:516 512▕ ! is_null($url = call_user_func($this->missingNamedRouteResolver, $name, $parameters, $absolute))) { 513▕ return $url; 514▕ } 515▕ ➜ 516▕ throw new RouteNotFoundException("Route [{$name}] not defined."); 517▕ } 518▕ 519▕ /** 520▕ * Get the URL for a given route instance.

  +2 vendor frames 

3 tests/Feature/UserTaskTest.php:11

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\UserTaskTest > it does not allow users to access admin task page RouteNotFoundException
Route [admin.tasks.index] not defined.

at vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php:516 512▕ ! is_null($url = call_user_func($this->missingNamedRouteResolver, $name, $parameters, $absolute))) { 513▕ return $url; 514▕ } 515▕ ➜ 516▕ throw new RouteNotFoundException("Route [{$name}] not defined."); 517▕ } 518▕ 519▕ /** 520▕ * Get the URL for a given route instance.

  +2 vendor frames 

3 tests/Feature/UserTaskTest.php:19

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\UserTaskTest > it allows administrator to access tasks page QueryException
SQLSTATE[HY000]: General error: 1 table users has no column named is_admin (Connection: sqlite, SQL: insert into "users" ("name", "email", "email_verified_at", "password", "remember_token", "is_admin", "updated_at", "created_at") values (Ms. Sheila Jones, tiara61@example.org, 2024-12-15 17:17:52, $2y$04$fy2DVEpJlH9s4sWT0qQXuOBt4/HnMBiO1viWnT/erPBTY/haI5N4i, WuD3JgyXhf, 1, 2024-12-15 17:17:52, 2024-12-15 17:17:52))

at vendor/laravel/framework/src/Illuminate/Database/Connection.php:565 561▕ if ($this->pretending()) { 562▕ return true; 563▕ } 564▕ ➜ 565▕ $statement = $this->getPdo()->prepare($query); 566▕ 567▕ $this->bindValues($statement, $this->prepareBindings($bindings)); 568▕ 569▕ $this->recordsHaveBeenModified();

  +16 vendor frames 

17 tests/Feature/UserTaskTest.php:24

Tests: 3 failed, 25 passed (42 assertions) Duration: 2.39s

I'm not using sqlite, I'm using mysql.

M
Modestas ✓ Link copied!

First - why are you running this on SSH? Live servers should never have tests running in them :)

Secondly, there seems to be some things missing. For example - the is_admin column. This could say that there's an issue with migrations (they either never ran or there's a missing one). As for the route not found - double check that the route was defined in your routes list by using php artisan route:list

M
M ✓ Link copied!

I'm seeing an issue when trying to edit a task. auth()->user() seems to have dropped the roles/permissions in addGlobalScope. The line if (auth()->user()->hasRole(Role::Patient)) { is not working as expected - there are no roles or permissions attached to auth()->user() so the result is unauthorized.

use App\Enums\Role;
use Illuminate\Database\Eloquent\Builder;
 
class Task extends Model
{
    // ...
 
    protected static function booted(): void
    {
        static::addGlobalScope('team-tasks', function (Builder $query) {
            if (auth()->check()) {
                $query->where('team_id', auth()->user()->current_team_id);
 
                if (auth()->user()->hasRole(Role::Patient)) {
                    $query->where('patient_id', auth()->user()->id);
                }
            }
        });
    }
}

The tests don't see this. It thinks it is OK.

It fails even you dummy the line like: $dummy = auth()->user()->hasRole(Role::Patient); or if ( ! auth()->user()->hasRole(Role::Patient)) {

What I think may need to happen is to set the session with setPermissionsTeamId:

            if (auth()->check()) {
                $query->where('team_id', auth()->user()->current_team_id);
 
								setPermissionsTeamId(auth()->user()->current_team_id);
								// auth()->user()->unsetRelation('roles')->unsetRelation('permissions'); // this wasn't needed

                if (auth()->user()->hasRole(Role::Patient)) {
                    $query->where('patient_id', auth()->user()->id);
                }
            }

Alternatively, I found that the middleware in chapter 10 needs to refresh the cache - app/Http/Middleware/TeamsPermissionMiddleware.php:

        if (! empty($user = auth()->user()) && ! empty($user->current_team_id)) {
            app(PermissionRegistrar::class)->setPermissionsTeamId($user->current_team_id);
						auth()->user()->unsetRelation('roles')->unsetRelation('permissions');
				}

poss doing both, since the middleware may not be run in certain situations.

VS
Vishal Suri ✓ Link copied!

Hi, do you also find it unusual that roles and permissions are being lost without a clear reason? I’m facing the same issue and feeling a bit confused about what the standard solution should be. Ideally, we should be able to trace the root cause properly to prevent this from happening again.

VS
Vishal Suri ✓ Link copied!

Hi Team,

I’ve noticed that roles and permissions are being lost without a clear reason, and I find this quite unusual. I’m currently facing the same issue and feeling a bit unsure about what the standard solution should be in such cases.

Ideally, we should be able to trace the root cause properly to prevent this from recurring. Would it be possible for someone to take a look and help identify the underlying reason?

Thanks in advance for your support.

VS
Vishal Suri ✓ Link copied!

One solution which I found is refresh the roles and permission, public function update(User $user, Task $task): bool { $freshUser = User::with('roles.permissions')->find($user->id); return $freshUser->hasPermissionTo(Permission::EDIT_TASK); }

and It is not ideal to load permission again and again, so kindly assist and guide.