Let's Build Task Tracker App With Laravel | Part 2: Core Logic & Recurring Tasks System

Program With Gio| 04:52:22|Feb 18, 2026
Chapters19
Part two expands the production-ready daily task tracker with core category and task management, route model binding, API resources, strict authorization, and a focus on scalable, secure design including a custom console command for recurring tasks and lifecycle refactoring.

Gio builds a robust Laravel task tracker focusing on architecture, security, and scalable patterns like route model binding, UUIDs, policies, and API resources.

Summary

Program With Gio’s Part 2 dives deep into turning a simple task tracker into a production-ready app. Gio emphasizes software architecture, security by default, and scalable decision-making over quick hacks. He reviews how part one laid the foundation with a custom authentication flow, Docker, and database design, and then shows how to implement core category and task management, route model binding, and strict authorization policies. The episode walks through resource controllers, form requests, and API resources to control data flow to the frontend, plus route key customization using UUIDs for public URLs. A significant portion covers recurring tasks: creating a custom console command, chunking large datasets, and planning a future more flexible recurring system with a dedicated recurring-tasks table. Gio also demonstrates testing, UI generation with AI tools, and refactors to mirror real-world SDLC—highlighting how requirements evolve and why architecture choices matter for security and scalability. He emphasizes the hybrid use of IDs (internal integers for DB relations and external non-sequential IDs for public endpoints), discusses gates vs. policies, and demonstrates how to implement and test authorization at multiple layers. The video is dense with practical Laravel tips: route model binding, withTrashed, mass assignment, API resources, pagination, and the distinction between behavior for views vs. JSON APIs. Finally, Gio previews future enhancements like a full UI for recurring templates, advanced scheduling with Laravel's task scheduler, and the plan to extract concerns into dedicated commands and services.

Key Takeaways

  • Use Laravel's resource controllers to auto-generate standard RESTful routes (index, create, store, show, edit, update, destroy) and tailor them with except/only as needed.
  • Adopt route model binding to inject model instances directly into controller actions, reducing boilerplate lookups and enabling clean method signatures.
  • Implement UUIDs (UU IDs) for external URLs while keeping auto-increment IDs for internal relations, and configure route key name to UU ID for proper binding.
  • Leverage Laravel’s form requests for validation, and align controller logic with policies to enforce user-owned data access securely.
  • Prefer API resources to shape data for JSON responses and to decouple front-end from internal DB structure; this also helps future-proof blade views and SPA front-ends.
  • Build a recurring-tasks system with a dedicated table, a generator command, and chunking techniques to scale tasks generation without memory issues.
  • Differentiate between gates and policies: use gates for global permissions and policies for model-instance permissions (ownership-based access).

Who Is This For?

Essential viewing for Laravel developers who want to graduate from quick prototypes to production-ready apps. If you’re building multi-tenant data, need robust authorization, or plan recurring tasks, Gio’s walkthrough covers patterns and pitfalls that docs often skip.

Notable Quotes

"The goal isn't just to write code that works, but to write code that scales and is secure by default."
Gio sets the tone for building production-ready software.
"We are going to define a single resource route... and this is going to use category controller which we'll create."
Demonstrates using Laravel resource routes and auto-generated CRUD.
"Laravel does have such capability and it's called route model binding."
Explains how Laravel loads models automatically.
"Using nonsequential non-guessable identifiers like UU IDs or UL IDs..."
Discusses security-conscious IDs for public URLs.
"The goal is to check ownership with policies so a user can only touch their own categories."
Illustrates authorization layering and policy usage.

Questions This Video Answers

  • How do I implement route model binding with UUIDs in Laravel?
  • Should I use API resources for blade views or only for JSON APIs in Laravel?
  • What is the difference between gates and policies in Laravel, and when should I use each?
  • How can I scale recurring task generation in Laravel without hitting memory limits?
  • How do I safely expose IDs in URLs without leaking internal database keys in Laravel?
LaravelRoute Model BindingUUIDsLaravel PoliciesForm RequestsAPI ResourcesEloquent ORMRecurring TasksLaravel CommandsChunking
Full Transcript
Hey everyone, welcome back to part two of the daily task tracker build. We're building a production ready task tracker app from the ground up using Laravel. But just like I said in part one, we aren't just hacking together a simple crowd app that can be done in under an hour, especially with uh AI tools. Instead, we are focusing heavily on software architecture, security, and the decision-making process that goes into building professional applications. The goal isn't just to write code that works, but to write code that scales and is secure by default. In the last video, we laid the foundation. We designed our database, set up Docker, and built a custom authentication system handling everything from rate limiting to timing attacks without using any starter kits. Now, in part two, we're going to continue on that buildup and start building out the main business logic for our application. We're going to build the core category and task management features. will implement route model binding to clean up our controllers and we'll use API resources to strictly control how our data flows to the front end. We will also lock everything down with strict authorization policies, ensuring users can never touch data that doesn't belong to them. We'll even dive into the differences between the gates and policies so you know exactly when to use which. We're also going to build a custom console command to handle recurring tasks. Set it up with Laravel to run automatically and we'll optimize it using chunking method to ensure it can handle thousands of records without crashing. We'll also do some refactoring to demonstrate the real life software development life cycle, showing you how code and requirements evolve over time. As you can imagine, there is a lot to cover in this one. So without further ado, let's dive right in. All right, so let's begin by building out the full CRUD or create, read, update, and do functionality for our categories because I think that's easier than tasks and it will give us strong foundation. So let's think about the routes that we're going to need. A user needs to be able to see all of their categories. They need to view a form to create a new category, actually store or save that category, then view a form to edit an existing category and update it after they make some changes. And finally, they should also be able to delete it. That's a lot of routes to define manually. Of course, we can do that and we can define all of those routes manually, which is perfectly fine. But luckily, Laravel has a really nice shortcut for this called resource controllers. So, in our web.php routes file, instead of defining six or seven separate routes for our categories, we're going to define a single resource route. So we will go within the offverified uh middleware group here because we only want authenticated users that have verified their email access the routes defined here including the category related routes. So we'll do route resource and call it categories. And this is going to use category controller which we'll create. And what's going to happen here is that this will automatically generate all the needed routes for a resourceful controller. If you're ever curious on what routes this generates, you can run the command PHP artisan route list to find out. So we'll do PHP artisan route list. We're going to get the error that the controller doesn't exist. So let's create the controller quick. We'll do PHP artisan make controller category controller and we will use d-resource option to create a controller with all the standard restful methods already stopped out for us. So this will essentially map to the routes that are defined in here. So let's generate that and let's uh import it in here and let's try the route list again. And sure enough we get list of routes and we have the categories right here. So we have categories index, categories store which is the post categories route, categories create, categories show, update, destroy and edit. And as you notice these are calling the methods edit, destroy, update, show, create, store, and index. And if we go into this controller, when we use that d-resource, it created all these methods for us and it stopped it out as well. like the store method accepts request show edit and some of the others receive the category ID as the argument and so on. So having this uh defined as a single route is uh kind of clean, right? We're not defining a lot of routes here and it also saves some time. Now you might not always want all of these routes defined in every single application that you work on. For example, we may not need the show route for the categories since they're simple entities and we technically don't need it. So we could exclude specific routes using except method. So we'll do except and pass a list of routes that we want to exclude. So in this case we'll exclude show or if you only need a few routes you could use the only method. All right. Now going back to the controller, there are some improvements that I'd like to make here before we even start writing code. First, I'd like to use form requests. We established earlier that we don't want to use uh generic requests here because then we will have to validate it here and it's just good practice to extract the validation logic into a form request. Like I mentioned before, you don't have to do that for every single application, every single controller. But since we're already creating form requests for the other controllers, it's better to stay consistent and do it for all. I also don't want to accept the category ID in some of these methods. Instead, I want the proper Laravel model to be injected and provided to me so that I don't have to do the lookup myself. Now imagine in the show and in the edit and in some of the other methods we'd have to run some kind of query in each method to select that specific category model. Wouldn't it be cool if we could accept something like category category and have Laravel figure out how to pull this and load it for us? It will still run the query behind the scenes of course, but we don't have to do that in every single method. Laravel will just kind of figure it out. So if we replace this everywhere where we have ID like this, it would be pretty cool, right? Well, I'm happy to tell you that Laravel does have such capability and it's called route model binding. When we type hint the model in here like this and then match the variable name to the route parameter name then Laravel will implicitly bind that and automatically load the correct model and then inject it in the controller like this so that it becomes available within the controller actions. Now use of route model binding is not a requirement. You can just accept the ID if you want to and do the lookup yourself. That's also perfectly fine and in some cases that might be beneficial for various reasons. You may want to do some custom lookup by that ID that the simple route model binding lookup is not sufficient enough. Now if we open the terminal again and look at the routes that our resource method defined, we see that these parameters in these routes are called category. So this name matches this name. When this is type hinted as the category model, Laravel will do the lookup for us behind the scenes. If you had soft deletes enabled on your model and you wanted to load the model even if it was marked as deleted and we'll talk about the soft deletion in a bit, you would add with trashed method on the route so that it doesn't give you 404 or model not found. So you would do something like with trashed. Soft deletes is something that you can enable on your models by adding the deleted at column which is the daytime column and the soft deletes traits in migrations. You can also call the soft deletes method which would create the column for you and the trait on the model basically just handles the deleted at column updates and automatically includes it in the queries to ensure that when you use eloquent query builder it automatically returns non-deleted records. When you delete a model by calling like a delete method on the model object, it will soft delete that meaning that it will just update the records uh deleted at column to the current time stamp or whenever that uh deletion action was performed. That way it's not going to remove the record from the database but instead update that deleted at column. And then when you run some queries to fetch the data, it's going to automatically add that where deleted at is no check for you and it's going to exclude those records that have been deleted. Soft deletes is a pretty neat feature that you might want to use on some of your uh models if you want to keep them in the table when user uh deletes them. for us. If the category is deleted, we're just going to mark the transactions that are associated with that category as having no category. So, we don't need to add the soft deletes feature to our category model. All right. The other thing that I wanted to mention is that instead of doing this manually everywhere like we just did it, it's not so bad. We replace that string ID with the category model category uh variable name. we could actually generate the model with the properly stopped out methods. Same goes for the request. Right now we have the request request but we want to replace these with appropriate form requests and we need to generate those form requests as well. We can actually do that by generating the controller along with its form requests and route model binding. So, let's delete this from here. And let's regenerate the controller. And we're going to add d-resource d-model and d- requests. Let's hit enter. Let's check it out. And that didn't work because I think this one needs the model to be specified, which I forgot. This has to be the category model. And I think that's going to work. Let me delete that. Hit enter. And sure enough, it created the controller along with two form requests. One for the store category, which is to create the category, and the other one is the update category that updates that category. If we inspect the controller, we see that the route model binding has been set properly and the methods are using the form requests. All right. So now that we're using the route model binding, let's test some of these methods out to make sure that they work. I want to specifically test maybe the edit method. Uh we said before that we don't need the show method because we're excluding the route here. So let's remove that method as well from here since we don't need it. But I want to test out this edit uh method. So what I'm going to do is I'm going to dy and dump the category here so that we can see if it properly loaded that model or not. The route as you remember is going to be categories slashcategory ID slashedit. So let's close that out. I'm going to create the category manually for now. So let's open the categories table. We are going to add a new row here. Let's set this to user ID one test and we can keep the rest null. Let's create another one. So we'll do user ID one test two. Let's call this test one. Update. And now let's go to the browser. Uh let's log in. 1 2 3 4 5 6 7 8. Let's go to slashcategories slash one slashedit. Hit enter. And sure enough, as you can see, the category model has been loaded successfully. If we click on the attributes, we see that we have the ID one, user ID one, name created and updated, which were set to null when we created them. If we change this to two, we're loading the category with ID two. Now you might notice a problem here. The problem is that in the URL I was able to go from ID 1 to ID2. That's because the category ID is or incrementing number and it's the primary key of the category. Right? So we are able to just increment the value and open the next category. And you might be thinking what's wrong with that? It's simple. It works. Let's move on. And you're not wrong. It does work. But I want to pause here and talk about a really important architectural decision that we sometimes make. Using auto incrementing ids for resources that are exposed in public URLs versus using things like universally unique ids or UYU ids or UL ids which are also unique identifiers but they're also sortable. Using sequential predictable IDs like 1 2 3 and so on in your URLs can leak information and create potential security problems. Number one, if I can access category slash one and category slash two, I can probably guess that there is a category slash3. An attacker could write a simple script to scrape all of our category IDs, giving them an idea of how much data the application has. They can also infer the total number of users by looking at the latest user ID and so on. This would also open up for enumeration attacks where they might be trying to guess other people's category IDs and try to access them. And while our authorization policies would prevent them from doing that uh from accessing other people's data, it still reveals some internal information. Also, your competitors could be using it to track your growth by observing how many new categories or users are created each day. To solve this, we can use nonsequential non-guessable identifiers like UU ids or UL ids. Now, you might be thinking, okay, so should we just replace the ID primary key with the UL ID? The answer to that is not exactly at least not in my opinion. My preferred approach is kind of a hybrid where we get best of the both worlds. I keep ID columns as integer auto incrementing primary keys for internal use and foreign key relationships and then I add separate unique UL ID or UYU ID columns that are used for any external facing URLs and API endpoints. integer primary keys are generally faster and more memory efficient for database indexes and foreign key joints than stringbased keys like UU ids or UL ids. So because of that I like to keep both the integer ids for internal foreign key and joins and relationships and things like that but also keep the second unique column ul ID or UU ID that I use for external facing APIs and URLs. So let's implement this. First, we need to add the UU ID column to our users tasks and categories tables because all of those resources or entities will be exposed in the URL one way or another. Maybe we're going to be making uh an Ajax call to update the user profile which is going to need the user ID. So we don't want to pass the user ID from JavaScript. even if it's not directly visible to the user, somebody can still inspect element and get the ID. So we want to ensure that any ID that's going to be exposed to the front end or in URLs or in APIs, they should be unique non-guessable ids. All right, so let's implement this. First, we need to add the UU ID column to our users, tasks, and categories tables. We can create a migration for that. So we'll do PHP artisan make migration add UU ID to tables. And to be clear, you can choose either UU ID or UL ID. It's up to you. It's just I prefer UU IDs in these scenarios. Let's open that migration. And we're going to do table users table UU ID. And we don't need to specify the column name because by default it's set to UU ID. We'll add this after the ID. And we want to make this unique because we want to make sure that no two users have the same uh UYU ID. We'll duplicate this and do the same for categories and for tasks. Let's add some spacing and let's update the down method. So, I'm going to put these here. And then we'll do drop column UU ID. And we'll do the same here and here. All right. Let's run the migration. And I'm going to warn you, this migration will fail. And I'll tell you why. So, let's run a PHP artisan migrate. And it's failing stating that there is a duplicate entry for the user's UU ID unique. How is that possible? Well, the reason this is possible is because we already have some users in the users table. And when we're trying to add the UU ID column to it, it's trying to create an empty string because we don't have any logic to generate the UU ID yet for the users or for any of these entities. And therefore when it moves on to the second user record it tries to create with another empty uh UU ID and it conflicts because of this unique constraint in here and therefore is throwing this error. Now we could generate the UU ids for existing users if we wanted to but since we're early in development I'm just going to run migrate fresh to recreate all the tables and remove the users. And then we're going to add the logic so that when user gets created the UU ID is generated properly. So we'll do PHP artisan migrate fresh. This is going to drop all the tables and recreate them and it will add the UU ID column to these tables. If we open the users table now we see that we have an empty table but we do have that UU ID column. All right. So now when creating new users or new categories or new tasks for them to get the uyu ID automatically we need to tell Eloquent how to do that. We essentially need to tell Eloquent that hey anytime a new user category or task is created also generate and assign some UU ID. Now we could do that manually kind of uh by listening on the model event and then set the UU ID column ourselves but another cleaner way is by using Laravel's existing traits for UU ids and UL ids depending on which you are using since we're using UU ID we're going to use has UU ids trait that will handle all that logic for us if we weren't using the trait then like I said we would listen on the model events like the creating event right before the model is created it triggers that event and there we would say hey right before you create this model generate UU ID and assign it to this UU ID column on that model. You could also use the observer feature uh for the models and you can check that in the documentation if you want to but like I said there is a cleaner way to do that and that is has UUID's trait. If we inspect this trait, we see that it generates the new unique ID uses the string support class and calls UU ID 7 on it. Now, if we try to register the user, we're going to get an error and I'll tell you why. Let's open the browser. Let's try to create a new account here. So, we'll do goample.com. 1 2 3 4 5 6 7 8. register and we're getting UU ID doesn't have a default value error. This means that it's not really generating the UU ID and assigning it to the UU ID column properly. To show you how that works, if we open the category model and we inspect the base model here, we see bunch of traits that are being used. One of these traits is has unique ID trait. If we inspect that, we see that this one defines some methods like uses unique ids which is set to false. But this should be set to true when category uses unique ids. If we check this and then go here has unique string ids. We see that this method right here sets uses unique ids to true which is basically this right here. So it essentially sets this to true. What this method does is that whenever the model that uses this trait gets initialized, this method will get called and therefore it will set this uses unique ids to true. That's why if you try to search for this method in the source code, you may not find anything because it doesn't really get called directly. It's kind of like magic where initialize prefix here indicates that whatever follows afterwards is the trait name. So gets initialized, we need to call this and we can easily test that. If we die and dump here and add this trait to the user model and we go to the browser and refresh the page, we see that we get one. This means that this right here gets executed whenever the user gets initialized. If we just do dump and not die and dump, it will still get executed. As you can see right here, Laravel also has another feature called bootable trades and you can check on that in the documentation uh if you want to know more about it. But the point here is that Laravel sets this to true. So let's continue to see what happens afterwards. So if we go back here, we have the method that returns the value of that property. So we know now that this will return true because we just saw how this gets set to true whenever the user model gets initialized. Now if we go back to the base model and we search for the uses unique ids, we see that it's used within the perform insert method. We can continue to debug this by adding dump one in here and then go here and refresh the page. And sure enough, we're seeing this uh printed in the browser. This means that we're hitting this method right here. So, it's checking if it's using the unique ids, then it needs to set the unique ids. And then it does the rest of the insertion logic by firing the necessary events, updating timestamps, inserting the data, and so on. Now, let's see the set unique ids method. This brings us back to the has unique ids trait. And this one goes through the list of unique ids array. And for each of the unique ID columns, it is setting to this new unique ID. So this is the method that is defined by returning null here. But in here in the hasuid trait, it's returning the UU ID 7. So it essentially overrides that method. is the same method name new unique id new unique id now what is this unique ids method so if we inspect this this returns empty array within has unique ids which is the trait used in the base model however the user model uses has uu ids and the category model as well which doesn't have that method but if we go to has unique string ids this method defines the unique ids method right here. So this overrides that unique ids method with the empty array and therefore it returns whatever the values here. Let's see what columns it is returning by default. So first it checks if the unique ids are being used which is true. If it is it is returning the key name. Key name is essentially the ID. So this method returns by default this. Now if we go back and refresh the page here, we see that we're getting a different error. What's happening here is that Laravel by default will try to generate the UID and assign it to the ID column, the primary column on your table. But in this case, the ID column for us is the integer primary key. And we are having the UU ID in a separate column. And Laravel doesn't know that. We need to tell Laravel that we have a separate UU ID column that we want the UU IDs to be assigned to. And we're getting this error that the data truncated for column ID because our ID column is the integer column and it's not a string. As you can see, it's trying to insert this as the ID. Now what we can do here is that what if we just define this method in our model and override what it returns. We we're in control here, right? This is just a trait. So why don't we just go to the model and simply define that method in here. So we'll do public function unique ids and return UU ID. This way we're telling Laravel that UYU ID is our unique id that you have to assign the UYU ID to. Now when the set unique ids method is called, it is going to call the unique ids from the model that we defined which is just going to be UID and it is going to set it properly. In fact, we can die and dump here like this to double check. So if we go back and refresh, now we see that it's set to UU ID. But if we go back and comment this out, it is going to be set to just ID. So let's go back. Let's bring it back. I'm going to do the same thing in the category. And we'll do the same thing in the task. I would probably create a separate trait for this so we are not repeating it in all the models. But since we only have three models, I don't see this as a big issue. So we'll open the task model here and we'll do the same thing. Use t has view ids and define that method in here and that should be good enough. Now let's go in here and remove the die and dump. Let's close all of these out. Let's open the browser. And sure enough, the user was created. The email was sent. Let's verify our email. And we're logged in. If we open the users table, we see that the UU ID is properly set. Now, we also need to tell Laravel's router what to do when doing route model binding. So that instead of the ID, it uses the UU ID column that we created. Otherwise, this routes that we created here, the category ID that gets passed, it's still going to try to look up the category by using the ID column, which is the primary key. That's the default for Laravel. However, we can customize the route key by defining the get route key name in our models and then return the column name that we want. So, we'll do get route key name and we'll return UU ID. If we inspect this base model again and search for get route key, we see that by default it's returning the key name which is ID by default. However, if we define that, then it's going to return UU ID. Now, even if we tried to go to categories one/edit, we're getting 404. Now, we're getting 404 because the category doesn't exist anymore. If we refresh, we have an empty table. But let's create one. I'm just going to enter some random UU ID in here. So, let's do user ID 1 test and create. Now, let's go back here and refresh. And sure enough, we're still getting 404 because category with ID 1 doesn't exist. Technically, it exists with the primary key one, but it doesn't exist with the UU ID 1. If we take this, copy it, and put it in here instead of one. Now, this works. And like I said, if we didn't define the get route key name and commented this out, we would get 404 for the UYU ID. But if we used the auto increment ID, we would still be back to the same problem. So when you're doing this hybrid approach where you have both the auto increment integer key as your primary key and then the UU ID or ul as your unique string key then you have to define the proper route key name. So that way the route model binding is using the proper UU ID or ulum and not the primary key integer ID. So we need to duplicate this across the user and the task model as well. So let's put it in here and within task as well. Now as you notice these two methods are really good candidates to be extracted into a trade. But like I said since it's just three models I'm okay to keep them the way they are for now. All right. So now let's start implementing these controller methods one by one. Let's start with the index method of the category controller. This method supposed to list the categories. The job of this method is to display a list of all the categories that belong to the currently authenticated user. So the first thing that we'll do is that we'll inject a request object in here so that we could grab the user from the request. Now we could also use the O facade to get the user from, but I prefer to get it from the request. It's just more intuitive to me. So we'll do user equals request user. Now remember that this user essentially returns the user model. So we could call the methods that are available on that user model which means that we could actually call the relationship related methods on that user model. Remember that in the user model we defined the relationships called tasks and categories. So if we call the categories on the user model we should be able to get this relationship object in our controller. So we could do something like user categories. And notice that we have two categories available here. One as the property and one as the method. This is the magic property that's available to the user model. What happens is that when you try to access the categories property on the user model like this, it is going to fetch the categories behind the scenes from the database and then return it as the list or the collection of categories. If we go here, we see that we have it documented that it's going to return a collection of the category objects. However, if you try to call the categories method, this is not going to fetch the categories from the database. It's not going to execute the query yet. What this does is that it returns has many object which means that we need to chain some other methods or call some other method that is actually going to make the database call to fetch the categories. One such method is get. When you run get on the relationship, it is going to basically do the same thing as the categories property, it's going to fetch the categories from the database and return it as a collection or list of categories. If we inspect this, we see that it's simply running the get on the query and returns a collection. Now, another method that we can call on the relationship is called pagenate. And the pagenate method essentially allows you to get the pagenated results. It doesn't return all the categories at once. It only returns limited amount of categories or limited amount of data. So instead of get we can call paginate and paginate accepts some arguments here that are optional. You can keep the defaults and it will work just fine. The per page is how many categories you want to fetch per page. 15 is default which is fine for us. The columns is what you want to select. Page name is the uh query string uh parameter name that you want to define as. So if you want in the URL to be called page, this is essentially that it's going to try to get that from the query string for you and figure out for which page you're trying to fetch the data. The page is the actual page number that you want to pass and if it's not set it is going to figure out what the current page is and the same goes for the total. If it's not set it is going to figure out uh the total. This method as you can see returns uh an object of length aware paginator. This object essentially contains the results for the current page plus all the information needed to render the pagination links like the total number of pages current page and so on. Now another thing I want to mention is that when we call the categories on the user notice how we don't have to specify a user ID to this query because essentially whether we do get here or the page inate it is going to query the categories table and constraint that query to the user and the way it gets the user is by using the user from here because we are saying that we want the categories related relationship on the user model. And this categories method defines the relationship in a proper way where it automatically by convention uses the user ID on the categories table to match to the ID of the users table and therefore when you call this here or when you call get it is essentially going to constraint the results to a specific user. All right. So before we continue here, I want to test this out. So what we're going to do is we're going to dump the result of this paginate here and let's create maybe few categories. This is where Laravel's cedars can come in handy. Cedars are special classes that let you populate your database with the test data. Instead of manually creating the records through your application or by running SQL statements or maybe doing the inserts manually through the database tool that you're using, you can write a database sitter once and then run it whenever you need some fresh test data. Laravel provides a database seed class by default, but you can create your own specific cedars using the PHP artisan makeer command. This database sitter kind of acts as like a main sitter that you run PHP RTSMDbc seed and it's going to run through this uh run command and you can have other sitters running within this run method. Right now it's using user factory to create a test user and we're going to talk about the factories a bit later once we get to the testing. For now, I'm just going to use a database insert uh statement to create bunch of categories. So in here, I'm going to get rid of these for now. And we can either use the DB facade directly to insert the records directly to the categories table using the regular SQL statements or we can use the category model to do that. So we could do something like for i equals zero i less than 10 i ++ and we'll do category create and pass the list of columns that we want to create it with. So we'll do name category i +1 and the user ID can be a user in random order uh and we get the ID from that or we can just hardcode it to one for now just so that we can test out our pagenation and other parts and we'll get to the database sitters in more detail as well as the factories a bit later. So in fact instead of 10 let's create maybe 50 categories. And again we could move this into its own uh sitter called category sitter but like I mentioned we'll do that later. For now let's open the terminal and let's run PHP artisan db seed. Application is running in production. I don't think we are. Let's double check the environment app env set to production. Let's set that to local. Let's close it. Let's try to rerun. And sure enough, we see that it's sitting the database. And now, if we open the categories table, we should have about 50 categories in there. Uh, well, we do have 51 because we created one uh before. All right, I'm going to get rid of this one that was manually created. So, that way we just have those uh 50 categories that were created from the cedar. Now we could rerun the cedar again and this is just going to create another 50 categories. So if we refresh now we have 100. So let's invoke this index method by going to the categories page. So let's go in here. We're going to log in with the user. Let's go to categories and we see the output. Now please excuse the white background here. During the editing, I'll try to change the background of this so that it's not too flashy. So, if you see bits of white background in some parts, that's probably me adding some kind of the dark background to avoid having this uh flashy white background. So, in the output, we see the length aware pageator object here. And we see the items contains eloquent collection instance which has uh 15 items in there because we're only loading 15 items by default. That's the first argument of that page inate method. So if we open that we see that it's the list of 15 category model objects. And if we open each we see that it's just the category uh model object. Now in addition to the items we also see other information like how many records we're loading per page what the current page is the page name which is page the total which shows 100 last page is 7 which means that you can go to the next page and so on. So now we can pass page equals 2 here and this is going to give us the next page basically next 15 uh records. So if we go here we see that now we are on current page two and this returns the next 15 categories. So this way you can load the data page by page. So we can go to page 10 for example and we're getting empty items because the maximum page or the last page is the page seven. So if we go to page seven, this contains the final 10 categories. So let's go back in here and I do want to mention that there are two types of query builders in Laravel. In Laravel you can build queries without Eloquent. you can simply use the DB facade and call the table on it and that will return the query builder object which is kind of like a low-level database agnostic interface for building SQL queries. So if we open our database instead of running the create on the category model what we could do is that we could run the insert statement directly using the query builder. So we could do something like DB and then call table which accepts the table name as the argument. So we could do categories. And if we inspect this method in here, we see that this returns a builder instance. And this builder is from the illuminate database query. That's the query builder. When you use this query builder and you execute the query to fetch the data, it returns a collection of PHP standard objects instead of using eloquent models relationships and so on. We could essentially build a query here. So we could do where and build up our query and then eventually call get which is going to fetch that data. So for example, before I show you the insert version in here, I'm going to take this and go back to the category controller and let's do dump this and let's just simply call get without the wear. It should just select all the categories. So let's comment this out. And before we go to the browser, let me try to echo out some HTML tag here with maybe a darker background color so that we don't see that white flashy background. So we'll do it like that and then we'll close it up here. So as you can see this returns a collection where the items is essentially an array of standard PHP objects. As you notice this query builder doesn't care about model. It doesn't care about relationships and so on. It's just standard PHP objects or a collection of PHP objects. You can also pagionate this. So instead of doing get we could call paginate and this will work the same way and instead of returning a collection from the eloquent it returns a collection from the support name space. So if we go back and refresh we see that it's returning length aware paginator but the items is illuminate support collection where before if we use the eloquent it was eloquent collection. So if we open that up, we see that it's still the PHP standard objects. We can navigate through the pages as well and so on. Now on the other hand, when using models and Eloquent, you are using the Eloquence query builder, which is essentially a powerful abstraction built on top of this query builder. It returns the collection of Eloquent models, not just the PHP standard objects. So if we uncomment this in here and move this here and go back and refresh, we see that this is the illuminate support collection and this is the illuminate database eloquent collection. If we expand it, we see that these are PHP standard objects. And here we see that these are eloquent models. Now we could call the new query here just to see what we get. and go back and refresh. We see that the first one returns an object of the query builder from the illuminate database query and the second one returns the has many object which is essentially the query builder from the illuminate database eloquent. If we go back here and inspect this and then go to has many, we see that this one extends the has one or many. So let's dig into that. This one extends relation. Let's dig into that. And we see that the query here is an instance of builder and this builder is the database eloquent builder. On the other hand, this new query is directly from the database query builder. If we inspect the Eloquent query builder, we see that this query builder uses the base query builder instance and it provides some abstraction and additional features specific to Eloquent. Now in most cases you will be using the eloquent query builder and eloquent models if you of course are using the benefits of eloquent with its uh relationships, accessors, mutators, model events, models and so on. But when you need to write more complex queries or you need more control, you can use the query builder to build out your queries. That way you're not relying on eloquent or the relationships. You can simply just write your own queries. You could build up the query here using where using join and so on. Now you could of course do that on the eloquent as well. But eloquent also allows you to use relationships in addition to rest of these methods to build out your queries. Now we also touched on the two types of collection classes. One is the generic collection class from the illuminate support namespace and the other is the collection class from the eloquent namespace which is built on top of the generic collection class. It essentially is pretty much the same idea or the same strategy as the query builder, the base query builder and the Eloquent query builder. The difference is that the collection from the Eloquent extends the base collection. So if we check this code here, we see that the base collection is the support collection. The eloquent collection just provides additional features specific to eloquent and some obstruction over the base collection. All right, so enough with that stuff. Let's close this out. And I wanted to show you a way to create uh the records without using eloquent. So you could do something like DB table categories and then do insert and pass the data that you want to insert. You can also pass a multiple records here. So we could do one and two and put this into an array. And this is going to create two records at the same time. So when I run this right now, it's going to create 100 categories and not 50 categories because we're inserting two records for each iteration. So let's open the categories table. Here we have 100 categories. So after running this, we should have 200 categories and not 150. So let's run that. We're getting general error. EU ID doesn't have a default value. And that's one of the benefits of using Eloquent because we had it so that UU ids were being set automatically for Eloquent. When you're doing things manually, you have to do some of these things manually as well because you lose the convenience of the Eloquent that allows you to set some of the properties automatically. So we'll have to just set the UYU ID here to something. We can use the UU ID method on the string support class. And let's do the same here. Let's try to run it again. And this time it worked. Let's refresh. And sure enough, we have 200 records. So, as you can see, you have options. You can use the eloquent and get a lot of benefits. Or when you need more control and you want to write your own queries in a specific way, you can just use the regular query builder or just simply execute the insert statements this way. So let's revert this back to using the model. And let's close this out for now. All right. So, what I'm going to do here is we're going to get rid of these and we're going to get the pagenated categories and we're going to assign that to a variable called categories like this. And then we're going to return some kind of view something like categories index. And we'll make the categories to be available within that view. Let's not worry about the blade templates for now. We'll create them in just a bit. The next method that we're going to fill in is the create method. And this is essentially the form that we're going to see to create the new category. So it's just the blades template that it needs to return. So we'll do return view categories.create. The store method is where we'll actually save the category to the database. It handles the form submission, validates the data and creates the new category in the database. The validation here of course will be handled by the form request class here called store category request. And in here we're going to set the authorize to true because anyone who's logged in should be able to create a category. Then for the validation rules, we're going to only have a single field called name and this will be required. It will be string and it will be maximum 255 characters. Now with the form request ready, the implementation of the store method is actually pretty simple. We can get the validated data using the validated method on the request. So we can do something like category data equals request validated. The next step is to actually persist this into a database. We can again manually create the category using the DB facade and then doing DB table and then insert or we could create a new model and then assign the properties and create it that way. So we could do something like category equals new category like this and then assign the properties. So we can do name equals category data name and then do category user id equals request user ID and then call category save. This is going to create the new category and set the user. Another way you can associate a user to the category model is by calling the associate method on the relationship. So you could do something like user. So call the user method which will return the belongs to object and we can call associate method on it and pass the user model. What this will do is that it will set the attributes properly. It will set the ids essentially properly and set the relationship up. Now while this works you can do this in multiple ways. Like I said one of the ways was to do it manually by doing the insert statement on the query builder or you could do something like category and then create just like we did in the database and pass the category data and this will work as well. The only problem with this is that category data here does not include the user ID and category create is not going to set the user ID automatically. So you'd need to make sure to add the user ID here and then this would work. Another way and the user way to do that is by doing something similar to what we did here. We got the categories relationship and then we called paginate on it. We could do the same thing but call create on it. So if we inspect the query builder here from eloquent and search for create, we see that it creates the new model instance with the given attributes and then calls the saved method. Now one other difference I wanted to mention with using the create on the eloquent versus using insert on the query builder is that this is going to trigger certain events certain model events where when you're using the query builder it is not going to trigger the events. It's the same way it did not automatically fill in the UU ID for us and we had to specify that ourselves. It also doesn't trigger certain events that eloquent models uh trigger when you call save, when you call create and so on. You could also call the create quietly here to create this record without triggering the model events. And you can look into the eloquent model events to see what events can be triggered and what events get triggered throughout the model life cycle. So let's go back here and we'll do request user categories and then call create and pass the category data. So what we're doing here is that we're getting the categories relationship on the user model and we're calling the create method. Kind of the same thing that we're doing in here. We're getting the categories relationship and we're pagionating. And this automatically adds the user ID to the query to scope the result to the specific user because we're using the relationship. Same applies to here when we're using the categories relationship on the user model. It is going to apply the user ID automatically when creating the data. That's why the user ID is not needed to be part of this category data. It is going to automatically create this category for this user. Now after the category is created, we can redirect the user back to the index page and flash some kind of success message to the session so that user knows that the category was created and they can also see the category that was just created because we redirect them to the index page. So we can do something like return redirect route or we could do to route. I sometimes prefer the redirect route versus two route, but two route is shorter and it does the same thing. So why not? So we'll do redirect categories index with success category created successfully. We technically don't need this assignment in here because while the create method does return the category model, we really have no use for it here because we're not doing anything with it. If you wanted to do something with it, like return the ID back to the user, maybe via session or if this was a JSON request, you would return it as a JSON and so on. But in this case, we don't need it. So, I'm just going to remove that. And let's move on to the next method, which is edit. The edit method works similar way to the create. It's just going to return some kind of form to edit the category. So we'll return view categories edit and pass the category to it. The next one is the update method which is similar to the store method because we're updating the category with the new data and we have the update category form request. We're going to set this to true here for now. And we'll talk about the authorization and policies in just a bit. And then we're going to add the name here as well because we want the name to be required string and maximum 255. Now you will see some duplication here because the create category and the update category are pretty much identical because the rules are the same. When you have cases like that, you could extract some of the rules into some kind of trait if you want to abstract it and then reuse it. Or you could use inheritance if you wanted to have some kind of base category request and then extend it. But I prefer not to do that, especially for minimal duplications like this. I don't mind having this duplicated over two requests. If I had a ton of requests that were related to each other and they were defining the exact same rules, then yeah, maybe I would consider moving it into a trade or something like that to reduce the duplication. But this also gives me more control and ability to change this around without affecting other uh classes or other form requests that define the same rules. All right. Now, in the controller, we can do the similar thing we did for the store. So we did request user categories create and here instead of going from the user route we already have the category model available because of route model binding and therefore since we have the category model available we can just call update on it and pass the validated data. So, we'll do request validated and it's going to update whatever fields we've defined in the update category request. Once that's done, we're going to redirect the user back to the categories index page with a success message that category has been updated successfully. The destroy method is also very simple to implement because we just need to call the delete method. Just like the update method and the create method on the category model, we also have the delete method. So we'll do category delete. And once the category is deleted, we're going to redirect the user back to the index page with a success message category deleted successfully. And that's it. We have completed the category controller implementation. We have a working crowd controller for categories. Now, let's work on the blade templates. And I'm not going to handwrite them. Instead, we're going to use AI to generate it for us. We have all of this backend set up. We have the layout setup. We also have some components that we borrowed from the Largo breeze. So it should be pretty straightforward for an AI tool like cloud code cursor or even chpt to generate the front end for this application and also to match it to the existing components and the page layout that we already set up. I'm going to be using cloud code for this but you're free to use whatever you prefer or you could just handwrite it if you like to write some blade templates. Let's open the cloud code and I'm going to paste in the prompt that I've created before. Our prompt is simply going to be something like implement simple UI blade templates for the category controller under resources views categories. Use the app layout that we've already defined and the components that we already have wherever possible. You must use the Tailwind CSS version 4 and match to overall UI of the app. Refer to O pages for overall theme and colors and also light and dark mode support is a must. You can check how other pages or components implement it for context. Let's press enter and wait for cloud code to build the UI for us. I'm going to fast forward this and once the cloud code is done, let's check out the UI that it has built. So, we're going to go to the browser and looks like it added the categories page in the navigation as well. So, if we click on it, we're brought to the categories page. And honestly, it didn't do a bad job. This looks pretty good. Uh, we have the table displaying name created at and then some actions like edit and delete. We have a button to create the new category that takes you to the categories create page to enter the new category name and you can click on edit to go to a specific category with the UYU ID in the URL and update it and we can also delete it. It has the pagenation here. So we can go to next page, page two, page four and so on. So it's pretty good. Now let's go to page seven and we're getting an error called to a member function format on null. So it seems like for some of the categories the created date is not set and this is expected actually and I'll tell you why because remember when we ran the DB seed to seed our categories table we did it in two ways. One way we use the category model and another way we use the insert statement directly on the base query builder without eloquent. So within this database sitter if you remember we first did it this way and then we did the DB table insert. When you do the DB table insert like I mentioned before it doesn't have some of the features available that the Eloquent query builder does. For example, we noticed that with the UYU ID where we had to pass the UYU ID as part of the list of columns in here. And same applies to the timestamp columns. Laravel sets those up for us behind the scenes. And when you do the DB insert without the eloquent, they don't get set automatically. So, we have to pass those as well. And since we didn't those columns uh are now with the new in their created and updated at timestamps. So if we look at the created at we see that we have 100 categories with the new values and that's why we're getting that error. So what I'm going to do is I'm going to delete all of these categories that have null and let's remove that. And now we just have 100 categories. Let's go back here. Let's refresh. And we have everything working. However, that doesn't really fix the underlying issue here. That's kind of like a band-aid. We're assuming that we will never have created that column as null. What if we do? We don't want to break our page and our application just because one of the records has an invalid value. Cloud code didn't take that into consideration. And AI tools will almost never take those kind of things into consideration. That's why you should be in the driver's seat when using AI tools. So, we're going to go back and fix that up in just a minute. But Cloud Code actually did a pretty good job here. The design looks awesome. If we switched it to light mode, I'm sure it would work as well. I'm not going to test it right now in here to avoid flashing uh bright colors on the screen, but I'm sure it's working. So, let's go back here and let's review what it did for the UI. So, we'll open the resources, views, categories, and we have three blades templates, index, edit, and create. Let's open the index. So, it is properly using our layout. It has a slot header with the H2 categories and it's using Laravel localization which is not necessary here but yeah why not uh let it use that. Then it has a link to the categories create. It also displays the flashed message if the success message is in the session which is pretty good. And then it checks if we have the categories. Uh, I would have probably done something like categories is empty or is not empty instead of doing count, but this works as well. And then it loops through the categories and displays each category. It even uses the proper method in here for the delete, which is pretty good because our destroyer route is the delete route. And to make Laravel understand that it's the delete route, we have to pass this hidden field which this blade directive handles that for us. It adds the hidden uh field uh to the form. When the request comes in and Laravel tries to handle it, it detects that hey there is a method type on this request which is delete. So we need to match that to the delete route and not to a post route because as you can see the for method is set to post. But this is what's telling Laravel that this is actually a delete request. And you can do the same thing to patch and put. Then in here there is some JavaScript onsubmit return confirm. This essentially asks the user to confirm if we're sure to delete this category. And yeah I think this looks pretty good. Now, I'm not an expert in Tailwind, but I'm pretty happy with that. I mean, based on the quick uh look at these classes, they seem okay. All right. And then in here, we see this method called links on the categories. And that's what renders the pagionation. So, if we go back to the browser, this pagination right here is completely rendered by Laravel via this links method. So, you don't have to build that pagination UI yourself. This takes care of that. If we inspect that, we see that this renders a view with the given data. And you can pass your own custom view and data if you wanted to. Or if it's null, it will render the default view. And then if there are no categories, it also edit this section to display no categories found and a button to create the first category. I wonder how this is going to look uh with the other create button. Is it like you have a new category button here and a category button here that may not look that great, but I think uh it's fine. Let me see how it does it here. Yeah. So, I think it's going to display both buttons, which probably is not the best uh UX. Let me test that out quick. So, I'm going to delete that. Let's refresh the page. Yeah. So, we have it here. We have it here. Honestly, it's not too bad. So, you can go both ways. If you wanted to hide this, if there are no categories, we could essentially do something like if categories is not empty only then display this button. So now if we go back, we see that the button no longer shows. I think I'm going to keep it this way so we're not confusing the the user. We can now rerun the DBC. So, we'll do PHP artisan DBC. This will create 50 categories. Let's go back. Refresh. And now we have the button back in here. And we only have four pages because we only have 50 categories and not 100. Now, let's see the other components. So, we have the edit component. It has the form with the method put that makes a request to the categories update which is great. We have the input for the name and it also displays the error properly and we have the cancel and update category buttons. And for the create, it's probably almost the identical. I would have probably preferred to somehow not have a lot of duplicated markup in here. Like a lot of the HTML is the same. We could improve this by further prompting Cloud Code to reduce the duplication here and create a component, but I'm not going to spend a lot of time on the UI uh in here. We can do that off the recording. So, let's test some of these actions out. So, let's go back here. Let's test the new category. So let's click create. We get please fill out this field. That's the HTML required attribute. Let me remove that just so we can test the validation. So let's remove the required here. Go back. Click create. And sure enough, the error message is displayed properly. Now let's enter the valid name. Let's click create category. And we're getting mass assignment exception. And this is expected. And it's actually pretty good that we're getting this error. What's happening is that Laravel is protecting us from the mass assignment because we haven't defined the filable property on the category model and we haven't told Laravel that we are allowing the name column or the name property to be filled on the category model. and therefore Laravel detects this and throws the exception that someone is trying to pass a field that's not part of the fillable property. So what we can do is we can go back to the code. Let's open the category model and in here we're going to add the fillable property and we'll add the name to be fillable. Now, let's go back and refresh and retry. And sure enough, it worked. We get the category created successfully flashed message, which looks pretty good, honestly. And that category is probably all the way at the end. So, if we go to the fourth page, we see it right here. Now, I do want to show the new categories on the top. So maybe we can sort all of our categories. So we see the latest categories in here. And maybe also let's display the time when it was created because right now I can't really tell which one is latest. And maybe it would also be worth it to show the updated at column, right? Uh because we may want to know when the category was updated. For some reason, Cloud Code decided that that was not needed, but I think it's important that we have that. So, we're going to go back here and on the index we're going to add updated at. And like I said, a lot of the markup here is duplicated. And using AI tools to make these into components is probably one of the great uses of AI. So now let's scroll down here and we'll duplicate this as well. and we'll do updated at. And we need to fix it so that in case the field is null, we don't want it to fail. So we'll just make this be null safe in both places. And let's improve on the formatting. So right now it's displaying month, day, year, and let's do maybe time this way. And let's add it here as well. And if for some reason they're null, we can just display dash. And let's do the same here. Now let's go back here. And sure enough, we have the created at and we have the updated at. Now let's sort these. So if we go back to the category controller on the index method, we can say to get the categories and sort them by the created column. So we'll do order by created at in descending order and then pagionate. So we'll go back refresh and sure enough now we see that our new category is at the top. Now there is an easier way to do this. Instead of using the order by method we can just say latest and this will do the same thing. So if we go back and refresh it still works. What latest does is that it essentially calls the order by for us using the created at by default in descending order. So essentially whatever we were doing it does that for us. It's just a convenient method. You can also use oldest to sort by created at in ascending order and you can customize the column if you want to sort by updated at for example. All right. So now let's test the update method. So we'll go in here. Let's change this to test two update. And that works as well. All right. Now, let's try the delete button. And for some reason, the delete is not pointer and edit is pointer. I think this is Tailwind's defaults where it makes links be cursor pointer and the buttons uh don't have that cursor pointer. I believe it has something to do with the user experience or the better UI where pointer cursor indicates it's a link and not a button like a clickable link. Honestly, in this case, I think it makes sense for both of them to be um cursor pointers because it doesn't make sense for one to be a cursor pointer and the other not. The only reason this is a button is because we need the CSR of token and and it cannot be a link. So, in this case, it makes sense to add the cursor pointer. So let's go back and let's double check. Yeah. And now we have consistent cursor. Let's click delete and let's try to delete that. And sure enough, it worked. Category deleted successfully. All right. So one thing that I forgot to mention is that in the database sitter when we ran the DBC command, we were able to create the categories when we didn't have the fillable property defined on the category model. And we didn't get the same mass assignment exception in the console when we ran that command. The reason for that is because in Laravel when running the database sitters the mass assignment is disabled. All right. So now let me test the scenario where the created at is null. So I'm going to set this to null and I'm going to set this to null as well. Let's update. Let's go back. Refresh. That one is the category one. So, it's probably on the last page. And sure enough, we see dashes in here. Let's go back and update it. And now, if we go back, we see that the time wasn't updated. That's because when we clicked update, nothing really changed. If we go here and we update this like that and go back here, we see that now the updated at time stamp was updated. All right. So I'm going to maybe put this in the created that as well. And let's move on to another important topic called authorization. Now before I demonstrate the security issue that we have in our application and a major one if you ask me, I want to talk about the authorization versus authentication a little bit because there needs to be some sort of distinction between the authentication which verifies who a user is and authorization that determines what a user is allowed to do. Authentication is about identity. It answers the question, who are you? When you log into an application, you're authenticating yourself. You're proving your identity with the credentials like an email and password. Authentication is all about establishing that you are who you claim to be. In Laravel, this is handled by things like login forms, session management, password verification, and middleware like the O middleware that checks if someone is logged in or not. Authorization on the other hand is essentially about permissions. It answers the question what are you allowed to do? Once we know who you are through the authentication authorization then determines what actions you can perform. Are you allowed to perform this specific action on this specific entity or resource? Authorization is about checking whether an authenticated user has permission to perform a specific action or access a specific resource. In web applications, you can be authenticated but not authorized. For example, you might be logged into the system, so you're authenticated, but when you try to access someone else's private data, like someone else's categories, the authorization system says, "Nope, you don't have permission for that. You have proven who you are. You are successfully authenticated, but you're not allowed to perform this specific action on this specific resource. On the other hand, you generally cannot be authorized without first being authenticated because the system needs to know who you are before it can determine what you're allowed to do. There are some exceptions to this of course like when you allow guest users to view certain public content but in most cases authorization requires authentication first. We've handled the authentication already by properly authenticating the user but we haven't handled the authorization which checks if user is really allowed to do that specific thing. Now to demonstrate the major security flow here. Let's create another user. So we'll go here. Let's log out. We'll go to create the account. Let's do John Wick. That will be Johnwacample.com. Let's create the password and register. Now let's open the mail pit. So that's localhost 8025. Let's verify the email. We're logged in. Let's go to categories. And we have no categories, which is good. Let's create a category. John Wix category. And that works, which is great. Let's click edit. And let's copy this uh category ID. Now, let's log out from John Wick. And let's log in as Gio. Let's go to categories. Let's go to edit and let's change this to this category and click enter. And as you can see, we are able to access John Wick's category. And we can even change it. We can change this to John Wick's category updated. Click update. And that works. We can now go back to that category and confirm that it was updated. And sure enough, it was. We can also delete the category. So if we go here, we can inspect element. And let's simply change this ID to John Wick's category ID. And let's click delete. Okay. And sure enough, category was deleted. The category one is still here because that's not what we deleted. But if we now try to go to John Wick's category and hit enter, we get 404. So as you can see, this is a big problem, right? Me as the user Gio am able to see, edit and delete categories of another user, in this case John week. Now we could implement simple manual checks within the controllers or even within the form requests to ensure that the user is only able to take those actions on the categories that belong to that user and that would count as authorization. For example, in the category controller within the edit method and then within the update as well, we could do something like if the category user ID doesn't equal to the currently logged in user ID which we can get by O user ID or simply O ID. Then we can abort with the status code 403. The abort essentially throws the HTTP exception and then Laravel handles that exception in a way that it displays the proper error page for 403. Now if we go to the table here and maybe reassign one of these categories to John Wick. So we'll set this to two for example. And let's copy this and go to the browser and let's refresh this page. Now we're getting 403 because this no longer belongs to us. However, we only updated the edit method which means the user can still manipulate the form and still make the update and delete requests. Now to fix the other ones, we would have to duplicate this essentially and put it here and here and that would work. The other option as I mentioned is by updating the requests as well. So in here we technically don't need to duplicate this over here. We can take this and put it within here. So we can say if the category user ID is equal to the currently logged in user ID then return true. And the way we can access the category is by simply accessing it as a property on this uh object and Laravel will figure it out behind the scenes. So we could do something like this category user ID and this category will exist because Laravel will make that available. So let's go back and let's try to update one. So I'm going to copy this. Let's go back. Let's edit this and I'm going to inspect element in here. And we'll update the UYU ID in here. And let's try to change that. We click update and we get 403 which means we can v dump this value to make sure that we are able to access that user ID. Let's refresh and sure enough we get the user ID too. Now while this is okay and the controller version is okay as well there are better ways to handle that. Laravel actually gives us two different approaches for handling authorization and understanding when to use each one is really important for building well architected applications. Laravel provides gates and policies and many developers do confuse this as well. Think of them as routes and controllers. Gates are like routes. They are simple, direct and closure-based. You define them right in your service provider with a simple closure function. They're perfect for global permissions that aren't tied to any specific model or resource. Think about things like can this user access the admin dashboard or can this user upload files or does this user have access to the beta features or does this user have permission to create categories? These are systemwide permissions that don't really relate to a specific database record. Policies on the other hand are like controllers. They're classes that group related authorization logic together specifically around a particular model or resource. When you're dealing with questions like, "Can this user edit this specific post or can this user delete this particular category?" That's when policies become useful. They're designed to work with model instances and handle the complex authorization logic that revolve around ownership relationships and model specific rules. The great thing here is that you don't have to choose between the gates and policies exclusively. Most realworld applications end up using a mixture of both. You may also just use policies and never need gates and that's also fine. I typically use gates for those broad systemwide permissions and policies for anything that involves checking permissions against specific model instance. Like in this case, a policy would be a great fit because we want to check access to a specific uh model or a specific resource and that is the category. Does this user have access to this specific resource? In that case, the policy is the best option. The way I think about it in practical terms is if I'm asking can this user do something in general, that's usually a gate. If I'm asking, can this user do something to this specific model or database record? That's usually a policy. So, let's generate a policy for our category model. I'm going to get rid of this from here. And let's open the terminal. We'll do PHP artisan make policy category policy. And we need to specify the model for this policy. So, we'll do model category. This creates the category policy within the policies directory. So let's open that up in here. And as you can see, we have quite a few methods in here. We have view any, view, create, update, delete, restore, and force delete. And they all return false by default, but we can adjust that. Of course, this kind of mirrors the controller methods, right? The view any would be the index method. The view is the specific category page. The create is the ability to create new categories. The update is if you have access to updates the specific category and you have the category model passed to this as well as the user object. Same for the delete and restore. And again the restore and force delete that relates to the soft deletes option which we don't have and we don't really need all of these methods but they're there in case you need them. Now if we follow the Laravel's naming conventions then Laravel will automatically discover this policy and make it work with the category model. If you had non-standard uh names or your policies were in a different directory, then you would need to register them manually in the app service provider boot method. But with our naming conventions, this should work out of the box. So let's fill in the logic in here. The goal is simple, right? We already showed in the controller and the form request what we need to do. We need to check if this user has access to this category. Meaning that the only users that can do anything with this category is the ones that created that category. The design of our app does not permit users edit or manage other users categories. If we wanted to do that, we could, but that's not the goal of our application. Every user has their own categories and therefore they should be the only ones being able to manage the categories that they create. Now, like I mentioned before, we technically don't need all of these methods here. Like create method would return true. And it's kind of redundant because we want all the users to be able to create uh the categories. We don't need this much granular permission checks for this simple model. So that means we don't need this method. Let's get rid of that. We can keep this one for now. We don't need the create method because this will just return true since we want to allow any user to be able to create categories. Uh the update will keep the delete will keep restore and force delete don't apply to our category model because we don't have the soft deletes. And these three methods do make sense because we want to make sure that user can only view the categories that they have access to. they should only be able to update and delete categories that they have access to. So this means that we would have essentially the same checks here. So we would do something like the user ID equals to category user ID and we would do the same thing in all of these methods. Now another way that we could check this instead of uh hard coding this ID and user ID is by using is method. So you will do user is category user. This makes sure that the two models have the same primary key, the table and the database connection. Now the thing to notice here is that when we're accessing the user property this way, this is the relationship, right? This is the magic property that Laravel essentially runs the query and sets it up for us. This means that this would execute a query if the user wasn't loaded yet. And sometimes that's okay because you may use the user somewhere else throughout your request life cycle. But you may not want to execute this additional query if it's not needed. And in those cases, this might be the optimal way because this does not execute any additional queries. Another option is by using the ease method on the relationship itself. So we could reverse this and do something like this category user and do is and pass the user this way. And this is not going to run the additional query to select the user because remember this returns the relation right it returns belongs to relation and the is method is available on that as well. And if we inspect this is method is slightly different. It's not going to run the query when doing this comparison. It is going to compare the keys by getting the parent key and getting the related key from the model. We can actually double check that by logging the queries that get executed and we can do that by listening on the database events. Laravel actually provides a very easy way to do that. So we'll open our app service provider and within the boot method we're going to do something like DB from the DB facade listen and in the closure we get the query that was executed and we can type hint that to query executed from the illuminate database events and this event has access to the SQL query that was executed the bindings which is the parameter bindings that were used uh the number of milliseconds it took to execute the query connection and so on. So now in here we can simply log that. So we can do something like log info and log the SQL. Then in the context we'll log the bindings and the time. Now, let's go in here and I'm going to duplicate this and let me comment that out and we'll do it the other way first. So, we'll do user is category user. Let's clear out the logs from here and let's try it out. Let's go here. Let's refresh the page. Let's go back. And sure enough, we see the queries being logged. Now, this is not being logged because in here, this one essentially is the query to select the sessions. This is uh to select the user, which is a currently logged in user. And this one is to select the category that we are getting from the route model binding because we're accessing that specific categories update page. So, we are not logging anything related to this. The way we can make sure that this gets executed is by enabling this policy check on certain routes and actions. There are multiple ways that can be done. You can apply it at the controller level by calling the can or cannot methods on the user instance. So if we open the category controller within the edit method which shows the update form for the category we can do something like if o user cannot view the category then we want to do something like abort 403. You can also do this as a middleware within the routes and you can do it within the blade templates as well. I personally prefer the middleware option at the route level. Uh but let's test this out first and then I'll move it into the middleware uh in just a bit. So let me clear that up again. Let's go back. Refresh. Come back. Let's inspect. And these first three queries we already established that they always get executed because this is the sessions. This is the user that is authenticated and that's the category from the route model binding. Now we have an additional query to load the user. So this is the extra query. So we're executing for this check. Now let's replace it with this. And let's check it again. So let's delete that. Go back here. Refresh. Come back. Open that up. And we see that that another query that was selecting from the users is not being executed anymore. If we change this back and let me delete that again one more time just to confirm. Refresh. Come back. We see that extra query right here. And this one is just the activity update on the session in the database. So while…

Transcript truncated. Watch the full video for the complete content.

Get daily recaps from
Program With Gio

AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.