Implementing Pagination0:00
Yeah, we need to do something about this index sooner rather than later, because currently we are just loading every single Post in the forum onto one page, and as the forum grows as more people use it, it's going to come to a complete standstill and eventually it will crash the site. Let's turn to Laravel's pagination in order to fix this. The simplest way to implement pagination is to switch PostAllOut with PostPaginate, and that's a method built into all Eloquent models. If we do so and we refresh the page, everything disappears, and that's because the data structure between a paginator and a simple collection is very different. Let me show you what I mean. I'm going to take this information here, and I'll just return it directly, which will give us a raw JSON payload. So now when we refresh in the browser, well, we have the JSON here.
I'm going to take this information here, and I'll just return it directly, which will give us a raw JSON payload. So now when we refresh in the browser, well, we have the JSON here again that we can take a look at. Here's a data key, and it contains an array with all of the posts in the current page. If we scroll down to the bottom, we have another top-level key called meta, and this includes all of the information to do with the page that we're currently viewing. So what is the current page? What's the starting record on the page? We have all of the links that you'd want to be able to access, the specific page numbers. We have how many items there are per page, what the last record on the page is, and how many records there are in total across all pages. So we can use this data to implement our paginator at the bottom of the index. If you're using Livewire or you're using Blade,
many records there are in total across all pages. So we can use this data to implement our paginator at the bottom of the index. If you're using Livewire or you're using Blade, you can absolutely make use of the links method that every paginator ships with, and you won't have to worry about implementing it yourself. But if you're using Vue or React or some form of front-end framework, you likely will have to implement the paginator instance yourself. It doesn't take long, and you can reuse it across the board. So if you are using one of those frameworks, join me now as we build that paginator instance and get it working on the posts index. Now it would stand to reason, based on the structure we just looked at, that if we instead point our loop here to posts.data, well, we should again see a working index, and sure enough, we do. You'll also notice there are far fewer records, only
Building Vue Paginator2:10
at, that if we instead point our loop here to posts.data, well, we should again see a working index, and sure enough, we do. You'll also notice there are far fewer records, only 15 in fact, because this is just one page of the posts in our database. I'm going to go ahead and in the components directory, I'll create a new component called pagination.vue, and we can actually take this from another service like TailwindUI rather than building it from scratch ourselves. This one here is very nice. It's a great starting point. So I'll copy the Vue component, head back into the IDE, and paste it in place. We're going to need to grab the Heroicons Vue package in order for this to work. So let's go into the terminal, npm install Heroicons Vue, and once that's installed, we should be able to actually make use of this paginator right out of the gate. So I'll go back into
let's go into the terminal, npm install Heroicons Vue, and once that's installed, we should be able to actually make use of this paginator right out of the gate. So I'll go back into the posts index, and I'll go down to the bottom here, import the pagination component, and there we go. There is Tailwind's paginator. Of course, the data is static, so let's go ahead and update it to use the real data that comes from our posts.meta. Obviously, we'll need to pass that meta in, so let's have a property on pagination called meta where I can pass posts.meta. Okay, we'll jump into the pagination component, and we'll begin to update it. Let's ignore the mobile responsive stuff for now, and we'll come back to that in a few moments. First of all, let's update this little showing 1 to X of X records. We know that there is a meta.from property, a meta.to property, and also a meta.total property.
in a few moments. First of all, let's update this little showing 1 to X of X records. We know that there is a meta.from property, a meta.to property, and also a meta.total property. So we'll fill that out, and of course, we need to make that meta property available as a prop. So let's say define props, and let's just call that meta. Hopefully now, if we refresh, yeah, 1 to 15 of 245 results, which matches our expectations. Okay, let's focus on these links on the right-hand side. So I'm going to remove items, and instead, we can loop over the links inside the meta that we pass to this component. So here is an anchor tag. We can say v-for, and of course, we want link in meta.links. Okay, what will the href be? Well, the link contains a url property, so link.url. Here we have a class. We are going to have to change that slightly. I'm going to get rid of the span and data
the href be? Well, the link contains a URL property, so link.URL. Here we have a class. We are going to have to change that slightly. I'm going to get rid of the span and data here, and I'm going to replace it with v-html, which is going to be equal to link.label like so. Okay, obviously, this is not the finished result, but now we can see all of the links here, right up to 17, and then we have our next button as well. Currently, this is using rounded-lg. I'm actually going to change that and prefix it with :first-of-type, and I'll do the same thing for :last-of-type, but with rounded-md, and that should get rid of any issues to do with rounded corners. That looks great. I can remove, obviously, all of the extra links that we have here, so let's get rid of those. Okay, that's much better. I'm going to add a little bit more padding, maybe px-3 rather than px-2. We also
all of the extra links that we have here, so let's get rid of those. Okay, that's much better. I'm going to add a little bit more padding, maybe px-3 rather than px-2. We also need the active link style, which you can see here. So here are the classes we'll want, and yeah, they have default and active. So I'm going to grab the current ones, and then under here, I'll make use of the object class syntax in order to apply these only if the link is currently active. Okay, does that make sense? Only if the link is active am I applying these particular properties, and there's a little bit of duplication here. I think I can get rid of everything from here to the end. So from where we set the text color, get rid, and then we're going to have another property on the class object where it will contain all of these default classes, right? So we'll grab the default classes,
Switching to Inertia Links6:14
color, get rid, and then we're going to have another property on the class object where it will contain all of these default classes, right? So we'll grab the default classes, and let's paste them here. All of those only apply if the current link is not active. Now, there are other ways to do this, but this is the way I'm going to reach for. I like the object syntax for classes. All right, we should be able to get rid of that comment now, and let's see what we're left with. Okay, that's pretty cool. I can go ahead and I can click on different pages. You'll note that we're currently doing a full page reload. One of the reasons we use Vue is to have a single page application feel, so we'll need to update our code from using a basic anchor tag to using a link tag, and we'll import that down here. So import link from InertiaJS Vue 3, and now as we click around, there we
to update our code from using a basic anchor tag to using a Link tag, and we'll import that down here. So import Link from Inertia Vue 3, and now as we click around, there we go. That's a single page application. We're no longer reloading the whole page. Just to prove that to you, by the way, I can add preserveScroll. So if I add preserveScroll to this Link tag, and then we click around, now you can see the data is changing, but we're not reloading the page. We don't jump back up to the top. However, it makes sense that you do jump back up to the top if you are clicking on a pagination Link, so I'm going to return it to what it was. Rest assured, we are now in a single page application. Brilliant. Let's alter the style slightly. I don't like the white background, so we can come up to the top here and get rid of bg-white, but maybe we give this pagination instance, so the nav,
Fixing Mobile Pagination7:43
alter the style slightly. I don't like the white background, so we can come up to the top here and get rid of bg-white, but maybe we give this pagination instance, so the nav, where's that? Yeah. Let's give the nav bg-white. Yeah, there we go. That's looking good. Okay, let's focus on mobile for a second, so we come into mobile. We have previous and next, but we just need to update them to use the correct links, so we'll use link and link here. We need to grab the href from the meta, and I think we could use computed properties for this. I think that would make a lot of sense, so let's have a const called previousURL, and previousURL is a computed property. That computed property is going to grab the meta, so we can say const props = defineProps(), so we'll grab props.meta. We're looking for the links, and I just want the first link item, so 0.URL. All right, that's previous
meta, so we can say const props equals defineProps, so we'll grab props.meta. We're looking for the links, and I just want the first link item, so 0.url. All right, that's previousUrl. Let's duplicate that and handle nextUrl. In order to grab this, we'll have to reverse the links array, so let's call the reverse method. Again, grab the first item and the url property on that item, and now we can make use of previousUrl and nextUrl for the mobile links up here, so the href for this one will be previousUrl, and the href for this one will be nextUrl. Hopefully, that works correctly, so next takes us to page four, as you can see. Previous takes us ... That takes us back to page five. Wouldn't have expected that. Ah, hold on. I think reverse is actually altering the original array, so let's use this syntax in order to clone and copy this links array so that we don't affect
Customizing Pagination Labels9:19
have expected that. Ah, hold in. I think reverse is actually altering the original array, so let's use this syntax in order to clone and copy this links array so that we don't affect that accidentally. Now if we go previous ... Oh, that's still going to the wrong page. Let's refresh just in case that's ... Yeah, there we go. Now I can go page five, page four, page three, but then I can go back to page four and back to page three. Okay, that's working perfectly. I don't like these arrows that are built into the paginator. That's actually coming from the text that comes across from Laravel. I'd rather handle that manually. The easiest way to solve this is to go to the terminal. I'm going to run php artisan lang:publish in order to publish Laravel's default language files, and now you'll find those files under your root directory, under lang/en, and then here we have pagination.php.
lang colon publish in order to publish Laravel's default language files, and now you'll find those files under your root directory, under lang/en, and then here we have pagination.php. There are also other files that get published. I'm actually going to delete those because I want to keep my skeleton as thin as possible, but in pagination.php, you'll see we have two keys that we're interested in, previous and next, and this little piece of code here, well, this is the double arrow that we see rendered on the front end, so I'm going to remove it, and I'll also remove the double arrow after the next button. Hopefully ... Yeah, there we go. Our previous and next buttons now read as they should. We can obviously alter those for different languages, and I'm very easily able to click between pages to move forward and pages to move backwards as well, and that puts us in good stead for the
alter those for different languages, and I'm very easily able to click between pages to move forward and pages to move backwards as well, and that puts us in good stead for the rest of the series because we're going to be dealing with paginated data in a number of places, and having that pagination component available, well, it's going to save us a lot of time. It's one of the great things about pretty much all front-end frameworks, Blade included. You can extract these reusable components so that you don't end up repeating yourself, and you maintain a consistent visual style that is easy to update and alter as the need arises. Okay, remember in the last episode, I said there were two things I wanted to handle in this episode? The first was pagination so that we weren't loading every post in the
Resolving N+1 Queries11:24
Okay, remember in the last episode, I said there were two things I wanted to handle in this episode? The first was pagination so that we weren't loading every post in the forum. The second was a performance issue to do with database queries that we'd created in the last episode. Let me show you the problem. I'm going to make use of a tool called Ray here by Sparcy. You can do the same kind of thing I'm about to do manually. You can do the same kind of thing I'm about to do with Xdebug, and you can also do it with something like DebugBar, which is a free plugin for Laravel, but I really like Ray. It's very easy to set up, very simple to get started with, and it will do exactly what we want it to do. From the terminal, I'll type composer require sparcy/laravel-ray so that I
it to do. From the terminal, I'll type composer require sparcy/laravel-ray so that I have all of the extra Laravel goodies. With Ray installed, I'll go back to our controller index method, and I'm going to call at the top Ray::showQueries(); What this will do is, whenever this particular index method is hit, Ray is going to log the queries to the Ray application for us to view and see what's actually taking place. Now, if we just look at our application once more, how many database queries would it take to load the data that we currently see? One. We should only be calling a single database query to grab the titles of all of these posts. So with that in mind, let's go ahead and reload this page, and now let's go into the Ray application itself and take a look at all of the queries that
of all of these Posts. So with that in mind, let's go ahead and reload this page, and now let's go into the Ray application itself and take a look at all of the queries that are actually being executed. Now, some of these queries, like the sessions query at the bottom, that's just an additional framework query that's going to happen because it's tracking the session. But all of these in the middle, that's our problem. This is the one we'd expect, right? You're loading all of the Posts, and you're paginating using the limit and offset keywords in SQL. But then it starts to grab users from the users table where user_id equals nine, where user_id equals seven, where user_id equals ten. Why is it grabbing all the users as well? Let me show you. I have the view debug tools available, so I'm going to jump into the view panel here, and on the index we have our posts.
Why is it grabbing all the users as well? Let me show you. I have the view debug tools available, so I'm going to jump into the view panel here, and on the index we have our posts. Here's the data. Let's open the first object. Sure enough, the title matches the first item in our list, and remember that user object that we talked about in the last episode? Well, it's here, and it includes the user's name and ID and profile photo URL, but we haven't eager loaded any of that data. So what Eloquent is going to do is when it comes across the user property inside our resource, it's going to say, well, I don't have that yet. I need to go to the database and fetch it, and it will do that each and every time for every post on a page. That is where all of these database queries are coming from. Each and every one is Eloquent saying, I don't have the user for this post. I need to grab it from
every post on a page. That is where all of these database queries are coming from. Each and every one is Eloquent saying, I don't have the user for this post. I need to grab it from the database. Now, there are two solutions to an N plus one problem. The first is to eagerLoad the data upfront. That would fix this issue. Let's take a look at doing that first and foremost. So where I say post paginate, I'm going to prepend a with method, and this is going to accept the relationship we want to eagerLoad. So I'll eagerLoad the user relationship on all of our posts. We'll come back to the browser and refresh. And now in Ray, rather than performing this database query to grab users N plus one times once for every post in our database, we do a single query to load all of the users in one go. Much faster and much more performant. So that's eagerLoading. But if you take a look at our post index again,
in our database, we do a single query to load all of the users in one go. Much faster and much more performant. So that's eager loading. But if you take a look at our post index again, we don't actually show any information about the user or author of each post. We probably will down the line, but for now we aren't doing that. So all of that data is wasted. Yeah, it's faster because we're eager loading, but we don't even need the information in the first place. So is there some way we can tell our PostResource not to load the user object if we haven't eager loaded it beforehand? Absolutely. Let's open up our PostResource and where we have the user object here, I'm going to wrap it in a method that JSON resources make available to us called whenLoaded. You pass whenLoaded the relationship name. Well, that's user. And then you wrap the execution inside a closure. So here's the closure. What this is going to say is, look,
called whenLoaded. You pass whenLoaded the relationship name. Well, that's User. And then you wrap the execution inside a closure. So here's the closure. What this is going to say is, look, if this particular relationship, if the user relationship on this Post isn't loaded, it doesn't exist. I won't load in this particular piece of information. And that means that we won't accidentally fall into the M plus one problem, because if the resource doesn't have the data, it cannot and will not make use of it. The whenLoaded method is super powerful for preventing those performance issues that you might otherwise miss. And it's only down the line that you start seeing huge performance problems on your indexes in your applications. To show that this works, I'm going to go back to the PostController, and I'm going to remove the eagerLoading that we performed previously. And then let's reload this index. Sure enough, nothing changes. It works as
I'm going to go back to the PostsController, and I'm going to remove the eagerLoading that we performed previously. And then let's reload this index. Sure enough, nothing changes. It works as it did before. But if we again go to Ray, note that we're not performing any additional queries to grab the users. In fact, we're performing no queries to grab the users, because we never loaded them beforehand. So we fixed the issue for this particular scenario. But we never want to fall foul of the M+1 problem. Again, we want to enforce eagerLoading where necessary. Thankfully, Laravel actually supports that. I'm going to go to the app/Providers/AppServiceProvider.php back down to the boot method where we declared that JSON resources shouldn't be wrapped. And underneath, I'm going to call the Model::preventLazyLoading method. This is going to do exactly what it says. If we try to lazy load that is access a relationship that we haven't loaded ahead of time, it will
to call the model preventLazyLoading method. This is going to do exactly what it says if we try to lazy load that is access a relationship that we haven't loaded ahead of time, it will throw an exception, we will see that during development, and we'll be able to fix it before the problem actually comes up in our live application. To show you it working, I will have to return our resource to its original form where we always load the user relationship, even if we haven't eagerloaded it. And then let's refresh this page. And look at that, we got an exception, because we attempted to load the user relationship. But lazy loading is disabled. Now, we haven't actually tested any of this stuff. We've been building out our pagination, we're passing paginated data down into the view, but we don't actually test that we kind of left it. Now that we have things in place, I'd like to come back and add some testing helpers to allow us to very
paginated data down into the view, but we don't actually test that we kind of left it. Now that we have things in place, I'd like to come back and add some testing helpers to allow us to very easily check that the correct data is being passed down to the front end. Let's tackle that in our next episode.
