In this lesson, let's talk about structuring projects by areas based on roles. It's a typical scenario for many projects: for example, you have administrator users and regular users.
There could be a more complicated scenario: for instance, in school management software, you might have students, teachers, admins, etc. So, how do we structure such projects?
We have two sub-questions I've seen developers asking:
app/Models/Admin.php
, app/Models/User.php
, etc?app/Http/Controllers/User
, app/Http/Controllers/Admin
, etc? Same with Routes?The answer to the first question is no. Please, please don't create separate models and DB tables for different user roles. It will bite you in the long run: you will need to repeat the same logic in multiple Models, then. Use one model User
and then roles/permissions DB tables for this: below, you will see a few examples.
The second question can be answered with "it depends on a project" or "it's your personal preference".
Some projects have totally separate admin areas: different designs, different features, separate login areas, etc. Then, of course, it makes sense to separate the Controllers/routes/views into subfolders.
Other projects are just about checking the permission of the users to access the same pages/functionality. In those cases, I would go for the same folder for Controllers/routes/views and check the permissions as needed.
Let's look at examples of this approach from open-source projects.
This approach to structuring admin/user areas is about using roles/permissions and not changing anything about the structure of the folders.
In other words, you have the same Controllers, common for all user roles. Just check the permissions of who can access which route or method.
The example is from ploi/roadmap, an open-source project.
They have a role
column in the users
table, and roles are defined in the enum
class.
2022_06_17_053959_convert_roles_for_users.php:
public function up(){ Schema::table('users', function (Blueprint $table) { $table->string('role')->nullable()->default(UserRole::User->value)->after('password'); }); // ...}
enum UserRole: string{ case Admin = 'admin'; case Employee = 'employee'; case User = 'user';}
Then, a method hasAdminAccess()
in the User Model checks if the user has the role of admin or employee.
class User extends Authenticatable implements FilamentUser, HasAvatar, MustVerifyEmail{ // ... public function hasAdminAccess(): bool { return in_array($this->role, [UserRole::Admin, UserRole::Employee]); } // ...}
Now, this method can be used throughout the whole project to check if the authenticated user has access.
app/Http/Controllers/BoardsController.php:
class BoardsController extends Controller{ public function show(Project $project, Board $board) { abort_if($project->private && !auth()->user()?->hasAdminAccess(), 404); return view('board', [ 'project' => $project, 'board' => $board, ]); }}
The second example is from the laravelio/laravel.io open-source project. The user role is defined similarly to the first example, but instead of the role
column in the user table here, it is called type
, and types are defined as constants in the User Model.
database/migrations/2017_04_08_104959_next_version.php:
public function up(): void{ // ... Schema::table('users', function (Blueprint $table) { $table->string('email')->unique()->change(); $table->string('username', 40)->default(''); $table->string('password')->default(''); $table->smallInteger('type', false, true)->default(1); $table->dateTime('created_at')->nullable()->default(null)->change(); $table->dateTime('updated_at')->nullable()->default(null)->change(); }); // ...}
final class User extends Authenticatable implements MustVerifyEmail{ // ... const DEFAULT = 1; const MODERATOR = 2; const ADMIN = 3; // ...}
Then, there are two methods to check if a user is an admin or moderator.
final class User extends Authenticatable implements MustVerifyEmail{ // ... public function type(): int { return (int) $this->type; } public function isModerator(): bool { return $this->type() === self::MODERATOR; } public function isAdmin(): bool { return $this->type() === self::ADMIN; } // ...}
Then, these methods are used, for example, in Policies.
final class UserPolicy{ const ADMIN = 'admin'; const BAN = 'ban'; const BLOCK = 'block'; const DELETE = 'delete'; public function admin(User $user): bool { return $user->isAdmin() || $user->isModerator(); } public function ban(User $user, User $subject): bool { return ($user->isAdmin() && ! $subject->isAdmin()) || ($user->isModerator() && ! $subject->isAdmin() && ! $subject->isModerator()); } public function block(User $user, User $subject): bool { return ! $user->is($subject) && ! $subject->isModerator() && ! $subject->isAdmin(); } public function delete(User $user, User $subject): bool { return ($user->isAdmin() || $user->is($subject)) && ! $subject->isAdmin(); }}
These policies can now be used in the Controller.
app/Http/Controllers/Admin/UsersController.php:
class UsersController extends Controller{ // ... public function ban(BanRequest $request, User $user): RedirectResponse { $this->authorize(UserPolicy::BAN, $user); $this->dispatchSync(new BanUser($user, $request->get('reason'))); $this->success('admin.users.banned', $user->name()); return redirect()->route('profile', $user->username()); } public function unban(User $user): RedirectResponse { $this->authorize(UserPolicy::BAN, $user); $this->dispatchSync(new UnbanUser($user)); $this->success('admin.users.unbanned', $user->name()); return redirect()->route('profile', $user->username()); } // ...}
And, of course, policies, in this case, are used in Middleware to allow only admins and moderators to access specific routes.
app/Http/Middleware/VerifyAdmins.php:
class VerifyAdmins{ public function handle(Request $request, Closure $next, $guard = null): Response { if (Auth::guard($guard)->user()->can(UserPolicy::ADMIN, User::class)) { return $next($request); } throw new HttpException(403, 'Forbidden'); }}
app/Http/Controllers/Admin/UsersController.php:
class UsersController extends Controller{ public function __construct() { $this->middleware([Authenticate::class, VerifyAdmins::class]); } // ...}
You may also read a few additional tutorials on this topic: