Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

kendrick's avatar

Architecture Question: Multiple-Guards or Permissions?

When thinking about the architectural fundament of an application, where we have at least two types of users, e.g. User.php and Doctor.php would you recommend defining guards within auth.php, or use something like laravel-permission, where we associate users with different roles.

Currently, I am using guards to authenticate users, and protect different routes. It works fine.

But then, when it comes to broadcasting real-time information, I am facing issues and broadcasting/auth (403) errors within my console, on at least one side of the two types.

I also can't imagine, how I could broadcast, let's say messages between the two guards, because they are using different sessions, and the broadcasting Channel would be protected through different guards, right?

And, when it comes to broadcasting, I am facing unclear behavior when I implement the guard on the channel.

Current architecture:

auth.php

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
 
        'door' => [
            'driver' => 'session',
            'provider' => 'doctors',
        ],
    ],

'providers' => [
        'minds' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ], 
        'doors' => [
            'driver' => 'eloquent',
            'model' => App\Models\Doctor::class,
        ],

    ],

web.php

Route::get('/users/home', 'UserController@home')->name('user.home')->middleware('auth');

Route::get('/doctors/home', 'DoctorController@home')->name('doctor.home')->middleware('auth:doctor');

What would you recommend also when thinking about interacting between the two types in real-time, as a secure architecture?

0 likes
38 replies
martinbean's avatar

@splendidkeen Keep users as users. Don’t create multiple models to represent users, as then you’ll find yourself having to create multiple guards, controllers, views, and so on. It all gets a bit messy when you need a route where more than one type of user can access a single route. Also, what do you do if you need to add another type of user (say, a moderator)? Do you start creating new guards, etc?

Instead, use roles and authorisation to determine what a user can see and do in your application. It stands to reason a doctor can also be a patient (they’re human; they have ailments too). Are you therefore expecting them to register twice in your application…?

I can say with over six years’ experience building Laravel applications of all sizes, that having a single user model in your application will make your life a lot easier.

2 likes
bugsysha's avatar

There are so many threads on this topic. Check responses there and you'll see that pretty much every thread gets replies like @martinbean posted. I can not recall the reason but I did that exact approach with multiple types of users with their own guards and it became hard to maintain pretty fast. Since then (2014) I've never created multi guard apps and try to avoid it at any cost.

1 like
kendrick's avatar

If I understand it right, then I would only have a users table for both doctors and users? Also, even when they register, they have to provide completely different information? Then I would e.g. for users leave the 'street' field (which is requested for doctors) blank?

And also if users and doctors have two different layouts? How would I structure that?

bugsysha's avatar

Then I would e.g. for users leave the 'street' field (which is requested for doctors) blank?

Extract street to a related table. That way you will only have records for the correct role.

And also if users and doctors have two different layouts?

HTML layout? When I have something like that most of the time I reach out for lookup maps or make includes in blade files dynamic. That way it will fetch only required HTML for that role.

1 like
kendrick's avatar

So basically I would structure the users table as simple as possible (first_name, ..., email, password, timestamps), which will fit both, users and doctors.

Any extra information, e.g. only for doctors, like street, city etc. I would create a new table for? How would I call this table (only for structural clarity): doctors_address or users_address, including the user_id?

Within User.php, I would then add a simple relation to this table and Address.php Model like:

public function address()
{
      return $this->hasOne(Address::class);
}

Layout-wise, currently, I am working it out this way in blade:

Users views:

@extends('frame.users.default')

@section('content')

@stop

Doctors views:

@extends('frame.doctors.default')

@section('content')

@stop


bugsysha's avatar

So basically I would structure the users table as simple as possible (first_name, ..., email, password, timestamps), which will fit both, users and doctors.

Yes!

Any extra information, e.g. only for doctors, like street, city etc. I would create a new table for?

Yes!

How would I call this table (only for structural clarity): doctors_address or users_address, including the user_id?

Name it as generic as possible. You have few options:

  1. If you app has hospitals which might some day require to have addresses then name the table addresses and use polymorphic relationship to fit any model.

  2. If it is going to serve only for users then you can name it user_addresses. In this case you can name it addresses also without user_ prefix to it, and make it only for users with user_id on it, and if it turns out down the road that you have to add it to hospitals then you can easily change it to polymorphic relationship.

Within User.php, I would then add a simple relation to this table and Address.php Model like:

Yes!

Layout-wise, currently, I am working it out this way in blade:

Merge that to one view and within that view use something like:

@extends('frame.users.default') // make this as generic as possible to serve multiple user types

@section('content')
    @includeIf('partials.' . $user->getType()) // this will load differences
@stop
1 like
kendrick's avatar

Thank you for the kind response. @bugsysha

Meanwhile, I have read some more about this architecture of roles and permission, here:

https://medium.com/swlh/laravel-authorization-and-roles-permission-management-6d8f2043ea20

which makes sense to me.

Now I asked myself, if this can also be solved more easily and scalable with this package:

https://docs.spatie.be/laravel-permission/v3/introduction/ ?

With respect to the layout, can I address the different roles within my e.g. navigation with an if/else logic like:

@if(auth()->user()->role_id == 1) // represents users

@elseif(auth()->user()->role_id == 2) // represents doctors

@endif

or is this uncommon?

And how would e.g. our AuthController look like, if we e.g. need to request the address for a doctor, but not for a user? Basically, it would need two views, meaning two forms, and also two methods, right?

Like:

AuthController:

postUserSignup // for our users

postDoctorSignup // for our doctors

both getting stored within the users table, with a different role_id? Signouts, then would be only required once.

bugsysha's avatar

Now I asked myself, if this can also be solved more easily and scalable with this package:

I've never used any packages for that since all of that can be resolved with just additional type or role column on users table and policies.

With respect to the layout, can I address the different roles within my e.g. navigation with an if/else logic like:

Yes, it can be solved like that and it is not uncommon, but when you add another type/role you will have to add another @elseif statement so it is not that OCP, while the suggestion I've presented with dynamic @includeIf will not require you to change that part of the blade file.

And how would e.g. our AuthController look like,

You can include input fields which are nullable or required only if user selects specific type/role. No need for two auth methods on controller.

1 like
kendrick's avatar

You can include input fields which are nullable or required only if user selects specific type/role. No need for two auth methods on controller.

But it wouldn't be false to divide it through two forms, when the type of user doesn't use their role within the form, but through e.g. dedicated links, and so views?

How would that look like though within the view/ method? I would hide inputs based on the user selected type, right?

And then within my Controller, again a simple if/else logic, like:

@if($user->role_id == 1) 

$address = Address::create([
    'user_id' => $user->id, 
    ...
    'street' => $request->input('street'),
]);

@endif

or just don't add the |required flag for any of those inputs within my validation?

Yes, it can be solved like that and it is not uncommon, but when you add another type/role you will have to add another @elseif statement so it is not that OCP, while the suggestion I've presented with dynamic @includeIf will not require you to change that part of the blade file.

You are right with OCP. Where can I learn more about includeIf?

@includeIf('partials.' . $user->getType())

This one is not clear to me, yet. Where do I specify getType(), and what triggers that something will be displayed then.

I've never used any packages for that since all of that can be resolved with just additional type or role column on users table and policies.

Thank you for the kind response. I would then create Policies for my methods e.g. to view, update something, rather than adding a permission_table (as presented within the medium article), where I list every routes' Controller and method, and then attach a role to this route, right?

bugsysha's avatar

But it wouldn't be false to divide it through two forms, when the type of user doesn't use their role within the form, but through e.g. dedicated links, and so views? How would that look like though within the view/ method? I would hide inputs based on the user selected type, right?

Not sure if I understand, but I would have a register form where I just ask for full name, email, user type (user or doctor) and password. Cause that is the minimum data you need to create an account. Everything else can be asked later or forced through some onboarding process.

And then within my Controller, again a simple if/else logic, like:

Nope. I would ask that later in a separate view/controller. No need to ask too much info which will maybe make potential users to run away from your app. Maybe someone just wants to see how it looks/feels before they decide if they will use it.

or just don't add the |required flag for any of those inputs within my validation?

Answered in previous section.

You are right with OCP. Where can I learn more about includeIf?

https://laravel.com/docs/5.8/blade#including-sub-views

This one is not clear to me, yet. Where do I specify getType(), and what triggers that something will be displayed then.

You define it on App\User.php. I try to keep encapsulation to maximum that is why I disable __get and __set on Models.

I would then create Policies for my methods e.g. to view, update something, rather than adding a permission_table (as presented within the medium article), where I list every routes' Controller and method, and then attach a role to this route, right?

Yes.

1 like
kendrick's avatar

Not sure if I understand, but I would have a register form where I just ask for full name, email, user type (user or doctor) and password. Cause that is the minimum data you need to create an account. Everything else can be asked later or forced through some onboarding process.

You are right.

Ok, I am already making some sketches to restructure the architecture.

You define it on App\User.php. I try to keep encapsulation to maximum that is why I disable __get and __set on Models.

Within the getType() method, I would then include the different files for the partials (folder). Like partials.errors, would include an error information on all views, if triggered, or specifically add something like a navbar specifically for $user?

This is what I don't get yet, how getType() looks like, sorry.

How would I protect different routes within the navigation for different type of users, without the if/else logic:

navigation.blade.php
@if($user->role_id == 1)        
<a href="{{route('settings.users')}}">
    <p class="p">Settings</p>
</a> 
@elseif($user->role_id == 2) 
<a href="{{route('settings.doctors')}}">
    <p class="p">Settings</p>
</a> 
@endif

Sorry for the many questions, but this is so interesting.

bugsysha's avatar
// App\User.php

public function getType(): string
{
    return $this->type; // this can be either 'user' or 'doctor' at the moment
}
// some blade file
@includeIf('partials.address-' . $user->getType())

For the above blade file, if the $user->getType() returns a string 'user' then it will try to load partials.address-user blade file, but since you said that users do not have address you will not create that file and it will not be included. While on the other hand, if the $user->getType() returns a string 'doctor' then it will try to include partials.address-doctor blade file which you will create since you said that the user can have address and it will show input fields related to address for that form.

Same applies for navigation. You can have partials.nav-user and partials.nav-doctor files and there you put different navigation links and @include('partials.nav-' . $user->getType()) will show only correct HTML for that specific user.

1 like
kendrick's avatar

Perfect. Thank you so much.

Where would I specify type? Within the users_table as an additional column to role_id which writes down the role in words, like e.g. user or doctor?

Not sure if I understand, but I would have a register form where I just ask for full name, email, user type (user or doctor) and password. Cause that is the minimum data you need to create an account. Everything else can be asked later or forced through some onboarding process.

If they have two different starting points to be redirected to, then I would use an if/else logic on the redirect->route within the singular method, right?

bugsysha's avatar

Where would I specify type?

On users table add column called type or role which is a string. No need to create additional table and complicate stuff.

If they have two different starting points to be redirected to, then I would use an if/else logic on the redirect->route within the singular method, right?

Nope. You can again use polymorphism, as for blade partial include, to decide starting points.

Try not to divide them with if statements. You are constantly tending to go back to if/else statements and I'm trying to show you how can you make things work for all roles that you have in your app and follow OCP.

1 like
kendrick's avatar

Nope. You can again use polymorphism, as for blade partial include, to decide starting points. Try not to divide them with if statements. You are constantly tending to go back to if/else statements and I'm trying to show you how can you make things work for all roles that you have in your app and follow OCP.

You are right, sorry. I have been working with those logics for a while now, that is why it is difficult to imagine it at this point.

How would that polymorphism look like within our method, when we attempt to login the user, like so:

if(Auth::attempt(['email' => $request->email, 'password' => $request->password], $request->remember)){
    return redirect()->route('welcome-' . $user->getType())); 
}

After reading the Policies documentation, where do I specify the the type or role_id for $user within our Policy. Would again think about if/else here. Or can I add the type to our specific route, which then is only available to users of a specify type?

I would also still protect the routes, which require authentication, with middleware:auth, correct?

That is what has been so clear within the medium article. We defined roles, and permission. And then added a middleware of RolesAuth, which handled the routes, based on the role_id provided within the permissions. If the role was wrong, we would receive a 403.

// get user role permissions
$role = Role::findOrFail(auth()->user()->role_id);
$permissions = $role->permissions;

// get requested action
$actionName = class_basename($request->route()->getActionname());
// check if requested action is in permissions list
foreach ($permissions as $permission)
{
 $_namespaces_chunks = explode(‘\’, $permission->controller);
 $controller = end($_namespaces_chunks);
 if ($actionName == $controller . ‘@’ . $permission->method)
 {
   // authorized request
   return $next($request);
 }
}
// none authorized request
return response(‘Unauthorized Action’, 403);

How can I make sure, that some routes are only viewable to e.g. the type doctor?

bugsysha's avatar

You are right, sorry. I have been working with those logics for a while now, that is why it is difficult to imagine it at this point.

Relax. We are all here to learn and exchange ideas/experience.

How would that polymorphism look like within our method, when we attempt to login the user, like so:

return redirect()->route('welcome-' . $user->getType())); // yes you can do this
// But when it is more complex and just using getType() is not enough
// you can always create some additional method
return redirect()->route($user->welcomeRoute()));

App\User.php

public function welcomeRoute(): string
{
    return 'welcome-' . $this->getType();
}

After reading the Policies documentation, where do I specify the the type or role_id for $user within our Policy. Would again think about if/else here. Or can I add the type to our specific route, which then is only available to users of a specify type?

Since only doctor can create addresses

class AddressPolicy extends Policy
{
    public function create(User $user): bool
    {
        return $user->isDoctor();
    }
}

class User extends Model
{
    public function isDoctor(): bool
    {
        return $this->getType() === self::TYPE_DOCTOR;
    }
}

I would also still protect the routes, which require authentication, with middleware:auth, correct?

Yes.

How can I make sure, that some routes are only viewable to e.g. the type doctor?

You have few options:

  1. Create middleware for each user type (DoctorMiddleware, BasicUserMiddleware).
  2. Just use policies in every controller method

Your middleware example is too complex in my mind. If you want to go with middleware then just create

// App\Http\Middleware\Doctor.php

public function handle($request, $next)
{
    if ($request->user()->isDoctor()) {
        return $next($request);
    }
    // here you can throw exception, redirect or what ever you want
}

// App\Http\Middleware\User.php
public function handle($request, $next)
{
    if ($request->user()->isUser()) {
        return $next($request);
    }
    // here you can throw exception, redirect or what ever you want
}
1 like
kendrick's avatar

Thank you for your kind words.

Since only doctor can create addresses

Perfect, this makes sense to me, now I can start digging into Policies, thank you.

Create middleware for each user type (DoctorMiddleware, BasicUserMiddleware).

How would I add this Middleware to a route?

Just use policies in every controller method

So you recommend, adding Policies Via Controller Helpers? E.g. within the Controller update-method:

$this->authorize('update', $post);
bugsysha's avatar
bugsysha
Best Answer
Level 61

How would I add this Middleware to a route?

Create middleware via terminal command

php artisan make:middleware Doctor

Add logic to middleware as described in previous post.

Add that middleware to protected $routeMiddleware in App\Http\Kernel.php

// App\Http\Kernel.php
protected $routeMiddleware = [
    'doctor' => \App\Http\Middleware\Doctor::class,
];

// routes/web.php
Route::middleware(['doctor'])->group(function () {
    // add your routes here
})

So you recommend, adding Policies Via Controller Helpers? E.g. within the Controller update-method:

That is one option or use middleware. Depends what you want/like.

1 like
kendrick's avatar

Add that middleware to protected $routeMiddleware in App\Http\Kernel.php

Perfect, thank you. This rounds up everything. Thank you for your awesome help @bugsysha.

That is one option or use middleware. Depends what you want/like

I will figure it out while trying. Creating a Policy to a Model is good, I think. php artisan make:policy PostPolicy --model=Post

If I have a dedicated Models folder, with subfolders for structure, could I add --model=Doctor\Post ?

bugsysha's avatar

If I have a dedicated Models folder, with subfolders for structure, could I add --model=Doctor\Post ?

php artisan make:policy PostPolicy --model=Doctor\\Post

Glad I can help.

1 like
kendrick's avatar

A final question which came to my mind was, would the let's say 'home' or 'settings' view, then be equal for every type of user, meaning the same route and controller method?

This would create very large Controllers, if e.g. doctors have collections, that users don't have. Then adding every collection through compact('', '', '', '') to the view, plus it would require creating blank [] for collections that aren't used at e.g. the users.

Or is it ok to then handle the e.g. homepages with different views and controller methods?

bugsysha's avatar

This would create very large Controllers

Then you are missing something. You can do it with small controllers, just probably have unpractical approach to do it.

1 like
kendrick's avatar

Then you are missing something. You can do it with small controllers, just probably have unpractical approach to do it.

Ok, and when e.g. doctors have an increased management perspective and within the 'homepage' collections like tasks or number reflections, which normal users don't necessarily have, we just add the collections to the Controller, and then again use polymorphism to load the layout based on the type.

Our users partials, then just wouldn't trigger let's say $tasks from our HomeController@home within their polymorphic partials, right?

Edited: For the folder structure would you recommend


settings
    -user
    -doctor

or

user
    -settings
doctor
    -settings

bugsysha's avatar

Our users partials, then just wouldn't trigger let's say $tasks from our HomeController@home within their polymorphic partials, right?

Yes.

For the folder structure would you recommend

If you do not have many files but everything will be within one file then I would go for the first folder structure. If you have many files then the second.

1 like
kendrick's avatar

If our doctor type of user, hasMany(Room.php) (rooms()), we would use user_id within the rooms_table, but when those rooms have further tables under them, would we use room_id as equal to the user_id or again user_id within those tables, below?

bugsysha's avatar

If our doctor type of user

You wanted to say other way around? User type of doctor?

Just use room_id and you can then use HasManyThrough to find deep related models.

1 like
kendrick's avatar

You wanted to say other way around? User type of doctor?

You are right. At deep related models, I would then add room(), hasManyThrough(Room.php), correct?

bugsysha's avatar

Yes, hasManyThrough(Room::class) not php.

1 like
Next

Please or to participate in this conversation.