Building A Blog In Laravel Vapor: Part 4
Sep 17th, 2022 By Taylor Perkins Full Stack Developer
Good morning on this fantastic Saturday! For once, I do not have any crazy distractions from between last post and this one. I would like to give a HUGE shoutout to Laravel Livewire, I am still catching up on watching Laracon Online 2022 but have had the opportunity to watch Caleb Porzio speak on some huge upgrades coming in Livewire! I am super stoked about the features I saw. When going to build a new project, one has to weigh the pros and cons, and while I love Livewire, it definitely doesn't quite have the same feel as an SPA with React or Vue. That said, some of the latest features coming out are going to address just this! If you haven't seen his talk yet go check it out. I can't do the new features justice in this article, you just have to see for yourself.
Alas, I digress. We are finally approaching the end to this simple blog application built with and for Laravel Vapor. Of course, this is just the core, simplistic build. I am going to continue iterating on this and will update you if I build out any fancy features I want to share. Today, we are going to discuss the last few pieces to get your own blog built in Laravel. We'll need to manage our users for sure. As this application stands, I don't have many compelling reasons to actually manage users. The one major goal I had in mind, was removing anything hard coded. Up until I completed last weeks post, I had post author information hardcoded. I updated a few things once I had User management implemented to remove those hardcoded values. This was my only reason for now, since I will be using this application for another Blog I plan on starting. Before we begin, I need to first dive into the most important part to users, and one of my favorite packages, Bouncer.
Roles And Permissions Using Bouncer For Laravel
I really enjoy the Bouncer package by Joseph Silber, it builds on top of and integrates seamlessly with Laravel's Authorization. The concept under the hood is pretty simple. Roles are essentially just buckets, you put Permissions in those buckets. It enables a Developer to really build out powerful and flexible dynamic permissions and roles. You can even define ownership to models, which we will not discuss in this article but is a very powerful feature. Before we begin implementing roles and permissions with Users, we need to define those roles and permissions. Let's take a look at my simple seeder for handling this. First run the artisan command. You may name your seeder whatever you like.
1php artisan make:seeder RolesAndPermissionsSeeder
Now let's take a look at the roles. Again, taking an iterative approach, I am keeping things simple. I have no complex requirements for my roles and permissions, so I actually don't have any permissions yet, only roles. Here's what my seeder looks like.
1use Silber\Bouncer\BouncerFacade as Bouncer; 2 3class RolesAndPermissions extends Seeder 4{ 5 protected $roles = [ 6 'administrator' => 'Administrator', 7 'standard' => 'Standard' 8 ]; 9 10 /**11 * Run the database seeds.12 *13 * @return void14 */15 public function run()16 {17 foreach($this->roles as $name => $title) { 18 19 Bouncer::role()->firstOrCreate([ 20 'name' => $name, 21 'title' => $title, 22 ]); 23 24 } 25 }26}
We use the Bouncer facade here to create our roles. we use firstOrCreate so that in the future when we create new roles and permissions, we can simply just run our seeder again and not have to worry about duplicate roles. The firstOrCreate method is actually just a standard Eloquent Model method, not specific to Bouncer. Now of course, run your seeder.
1php artisan db:seed --class=RolesAndPermissionsSeeder
You should now have your new roles. I only created the two for now. Of course, it may be obvious but the only real use case for our application currently is Administrator. The rest of Users are all going to be the Standard role, which essentially does nothing but explicitly state the User is someone other than an admin. The administrator will be the only one who can access admin dashboard to manage and create Posts and Users. For now, we're just going to assume that you are going to manually create your own admin user for when you launch your blog application, so we won't dive into that aspect. But I do want to look at assigning roles for all other users that take the time to register to your blog.
Assigning Default Role To Newly Created Users With Bouncer Using Observer
Once again, I mentioned this before that I have found so many use cases for succinctly handling logic like this. While one could dive into the RegisteredUserController (this is the default App\Http\Controllers\Auth\RegisteredUserController that comes with Laravel Breeze) and add this little code snippet to assign a default role to newly registered users. However, in my opinion, this is not a clean solution. As your application gets more complex, you may find that this logic may need to change, or you may have similar logic required in different areas for various reasons. So let's take a look at a more robust and reusable solution. If you don't have one already, go ahead and create a UserObserver and register it in EventServiceProvider. See the last post for more information on Observers.
1php artisan make:observer UserObserver --model=User
For now we really only need the created event handler. You can feel free to remove the rest or just leave them blank. Next let's see what we need to do in order to assign a role to newly created Users. Let's take one step back and look at the steps required for installing Bouncer.
1composer require silber/bouncer2php artisan vendor:publish --tag="bouncer.migrations"3php artisan migrate
Finally just simply add Bouncer's HasRolesAndAbilities trait to your User model.
1use Silber\Bouncer\Database\HasRolesAndAbilities;2 3class User extends Model4{5 use HasRolesAndAbilities;6}
Now let's get back to the UserObserver. This trait we just added gives us some methods to use that can help us with certain actions such as authorizing users or assigning roles or permissions to Users. Please take a moment to review Bouncer's README if you have not to acquaint yourself. Our Observer will actually be quite simple. In our Created event handler, we're going to simply just give all newly created Users the Standard role. Check it out.
1class UserObserver 2{ 3 /** 4 * Handle the User "created" event. 5 * 6 * @param User $user 7 * @return void 8 */ 9 public function created(User $user)10 {11 $user->assign('standard');12 }13}
It's important to use the "created" event here and not it's similar present participle version "creating". We actually need a User that is stored in the database in order to assign the User a role. But it really is as simple as that. Anytime you create a new User now, no matter what context, it is going to be assigned the standard role. This is a good starting point, because once we take a look at managing User models, we'll see how easy it is to change this if need be. While we could have just left things as they were since there are no requirements or definitions for what a standard role can or can't do, we're forward thinking here. Now let's take a quick look at managing the User resource with Filament.
Managing User Resources Using Filament In Laravel
In case you haven't read any of my other posts, or are not familiar with Filament, I am going to run through this pretty quickly. However, this is not to confuse you or leave out information, it is more along the lines of a testament to how awesome and powerful Filament really is. Please checkout Filaments' thorough documentation if you are left confused anywhere.
Pro tip: Really think through the requirements for your model before creating your UserResource. It wasn't a challenging transition, but I first created my UserResource using a simple Filament resource. This worked in the beginning, but as soon as I started adding roles and profile images to Users it outgrew a simple resource quickly.
First we will simply create our Filament Resource just as we did with Posts. We don't want a simple resource, and we may want to generate basic forms and tables for fillable fields. We can delete whatever is generated later if we don't need it.
1php artisan make:filament-resource User --generate
At this point, I don't remember which form or table fields did or didn't get generated. Here is what my UserResource looks like after all is said and done. I am going to leave out my profile image stuff for now, we'll dive into that on another post. I have left out the getRelations and getPages methods as they were not changed. You may leave them as defaults.
1use Silber\Bouncer\Database\Role; 2 3class UserResource extends Resource 4{ 5 protected static ?string $model = User::class; 6 7 protected static ?string $navigationIcon = 'heroicon-o-collection'; 8 9 public static function form(Form $form): Form10 {11 return $form12 ->schema([13 TextInput::make('name')14 ->required()15 ->maxLength(255),16 TextInput::make('email')17 ->required(),18 TextInput::make('title')19 ->maxLength(255),20 Select::make('role')21 ->label('Role')22 ->options(Role::all()->pluck('name', 'id')),23 ]);24 }25 26 public static function table(Table $table): Table27 {28 return $table29 ->columns([30 TextColumn::make('name'),31 TextColumn::make('email')32 ])33 ->filters([34 //35 ])36 ->actions([37 Tables\Actions\EditAction::make(),38 Tables\Actions\DeleteAction::make(),39 ])40 ->bulkActions([41 Tables\Actions\DeleteBulkAction::make(),42 ]);43 }44}
My table is pretty simple, didn't need to show anything for now other than the User name and email. In the form, the name, title and email are straight forward text inputs. Let's take a closer look at the Role though. We need to use the Bouncer Role model in order to pull in our select options for assigning a role to the User. Filament makes using select fields super easy, we just query for all Role models created by our seeder, and pluck a key value collection out of our query. The value will be the Role id, and the label for the select input will be the name of the Role. Now let's take a look at our only other modification to handling User resource updates, which is updating the roles from the values chosen. Both of the following code snippets are implemented in your EditUser class (App\Filament\Resources\UserResource\Pages\EditUser.php).
1protected function handleRecordUpdate(Model $record, array $data): Model 2{ 3 if (isset($data['role'])) { 4 $record->roles()->detach(); 5 $record->roles()->sync($data['role']); 6 } 7 8 parent::handleRecordUpdate($record, $data); 9 10 return $record;11}
For now we're keeping things simple. We override the parent's handleRecordUpdate in order to customize the logic required to sync our new roles to the User model. we check if there is a role field in the data submitted. We are going to assume our User model only ever requires one Role, so we then detach all Roles, and sync the new Role. Then call the parent handleRecordUpdate so everything happens normally as if you never overrode this function and return the $record (User Model). Now for the final piece. We want the form data to be filled when the user already has a role.
1protected function mutateFormDataBeforeFill(array $data): array 2{ 3 $user = User::find($data['id']); 4 5 if ($user && $role = $user->roles()->first()) { 6 $data['role'] = $role->id; 7 } 8 9 return $data;10}
We override the parent mutateFormDataBeforeFill method in order to achieve this. We find the User model from the $data array and then check to see if that User already has a Role. We are assuming for now the User will only ever have one Role, so we just grab the first one. Assign to the role key of $data array, and voila! When you go to edit your User from the Filament dashboard, you should see that the select option is already filled with that User's current Role.
Conclusion
I really thought we would have enough time this post to finish up my talk on our simple blog application in Laravel, but we have a few more pieces I want to go over before I conclude this series. This includes our first iteration of handling profile images with our User model and its associated alt tag. Remember, one of my requirements for this project is to be SEO friendly, as I imagine most bloggers would be at least reasonably concerned about. We're also going to ensure we have everything setup and configured for Laravel Vapor and look into some Gitlab CI for automating our deployments. CI/CD is an exciting topic for me and quite a handy tool. While it's not all that complicated to launch to Vapor yourself, this CI/CD setup is pretty much a set it and forget it. So next time you make any changes, you can just commit your code and push it and boom. Your changes are launched within 10 minutes. Let me me know if y'all have any questions. Until next time!
Please sign in or create an account to join the conversation.