All Projects → juampi92 → Cursor Pagination

juampi92 / Cursor Pagination

Licence: mit
Cursor pagination for your Laravel API

Projects that are alternatives of or similar to Cursor Pagination

Api Generator
PHP-code generator for Laravel framework, with complete support of JSON-API data format
Stars: ✭ 244 (+419.15%)
Mutual labels:  api, laravel, laravel5
Api Restful Con Laravel Guia Definitiva
Repositorio para el código base del curso "API RESTful con Laravel - Guía Definitiva"
Stars: ✭ 95 (+102.13%)
Mutual labels:  api, laravel, pagination
Laravel Vue Starter
Well Documented Laravel Starter App From Development to Production. For Full Blown RESTFUL API and SPA with Beautiful UI Using Buefy / ElementUi For Reusable Vue Components
Stars: ✭ 76 (+61.7%)
Mutual labels:  api, laravel, laravel5
Laravel5 Jsonapi
Laravel 5 JSON API Transformer Package
Stars: ✭ 313 (+565.96%)
Mutual labels:  api, laravel, laravel5
Blade Migrations Laravel
An intelligent alternative version of Laravel 5/6 Database Migrations - uses raw-sql syntax, transactions, auto-rollback, UP-DOWN-UP testing
Stars: ✭ 25 (-46.81%)
Mutual labels:  laravel, laravel5
Larrock Core
Core components for LarrockCMS
Stars: ✭ 46 (-2.13%)
Mutual labels:  laravel, laravel5
Foocart
A Laravel 5 eCommerce application with integrated Stripe payments.
Stars: ✭ 8 (-82.98%)
Mutual labels:  laravel, laravel5
Laravel Jwt
Laravel with JWT Authentication for API development
Stars: ✭ 31 (-34.04%)
Mutual labels:  api, laravel
Laravel Widgetize
A minimal package to help you make your laravel application cleaner and faster.
Stars: ✭ 791 (+1582.98%)
Mutual labels:  laravel, laravel5
Binding Of Isaac Api
A RESTful API for the Binding of Isaac game series
Stars: ✭ 11 (-76.6%)
Mutual labels:  api, laravel
Laravel Highcharts
Laravel 5 Highcharts Package
Stars: ✭ 31 (-34.04%)
Mutual labels:  laravel, laravel5
Laravel Settings
Simple Settings package for a laravel application
Stars: ✭ 45 (-4.26%)
Mutual labels:  laravel, laravel5
Barryvanveen
📰 Personal blog of Barry van Veen. Focuses on Laravel Framework, website optimization and natural computing.
Stars: ✭ 24 (-48.94%)
Mutual labels:  laravel, laravel5
Laravel Restaurant Pos
Restaurant POS
Stars: ✭ 27 (-42.55%)
Mutual labels:  laravel, laravel5
Angular5.2 Laravel5.6
Angular 5.2 and Laravel 5.6 Authentication and CRUD
Stars: ✭ 17 (-63.83%)
Mutual labels:  laravel, laravel5
Easyhttp
A Laravel HTTP-client to make HTTP request easier and log requests and responses
Stars: ✭ 20 (-57.45%)
Mutual labels:  laravel, laravel5
Lara Eye
Filter your Query\Builder using a structured query language
Stars: ✭ 39 (-17.02%)
Mutual labels:  api, laravel
Api Tester
API tester for laravel-admin
Stars: ✭ 38 (-19.15%)
Mutual labels:  api, laravel
Laravel Compass
A REST client inside your Laravel app
Stars: ✭ 1,002 (+2031.91%)
Mutual labels:  api, laravel
Python Api Development Fundamentals
Develop a full-stack web application with Python and Flask
Stars: ✭ 44 (-6.38%)
Mutual labels:  api, pagination

Cursor Pagination for Laravel

Latest Version Build Status StyleCI Total Downloads Software License

This package provides a cursor based pagination already integrated with Laravel's query builder and Eloquent ORM. It calculates the SQL query limits automatically by checking the requests GET parameters, and automatically builds the next and previous urls for you.

Installation

You can install this package via composer using:

composer require juampi92/cursor-pagination

The package will automatically register itself.

Config

To publish the config file to config/cursor_pagination.php run:

php artisan vendor:publish --provider="Juampi92\CursorPagination\CursorPaginationServiceProvider" --tag="config"

This will publish the following file. You can customize the name of the GET parameters, as well as the default items per page.

How does it work

The main idea behind a cursor pagination is that it needs a context to know what results to show next. So instead of saying page=2, you say next_cursor=10. The result is the same as the old fashioned page pagination, but now you have more control on the output, cause next_cursor=10 should always return the same (unless some records are deleted).

This kind of pagination is useful when you sort using the latest first, so you can keep an infinite scroll using next_cursor whenever you get to the bottom, or do a previous_cursor whenever you need to refresh the list at the top, and if you send the first cursor, the results will only fetch the new ones.

Pros

  • New rows don't affect the result, so no duplicated results when paginating.
  • Filtering by an indexed cursor is way faster that using database offset.
  • Using previous cursor to avoid duplicating the first elements.

Cons

  • No previous page, although the browser still has them.
  • No navigating to arbitrary pages (you must know the previous result to know the next ones).

Basically, it's perfect for infinite scrolling!

Basic Usage

Paginating Query Builder Results

There are several ways to paginate items. The simplest is by using the cursorPaginate method on the query builder or an Eloquent query. The cursorPaginate method automatically takes care of setting the proper limit and fetching the next or previous elements based on the cursor being viewed by the user. By default, the cursor is detected by the value of the page query string argument on the HTTP request. This value is automatically detected by the package taking your custom config into account, and is also automatically inserted into links and meta generated by the paginator.

public function index()
{
    $users = DB::table('users')->cursorPaginate();
    return $users;
}

Paginating Eloquent Results

You may also paginate Eloquent queries. In this example, we will paginate the User model with 15 items per page. As you can see, the syntax is identical to paginating query builder results:

$users = User::cursorPaginate(15);

Of course, you may call paginate after setting other constraints on the query, such as where clauses:

$users = User::where('votes', '>', 100)->cursorPaginate(15);

Or sorting your results:

$users = User::orderBy('id', 'desc')->cursorPaginate(15);

Don't worry, the package will detect if the model's primary key is being used for sorting, and it will adapt itself so the next and previous work as expected.

Identifier

The paginator identifier is basically the cursor attribute. The model's attribute that will be used for paginating. It's really important that this identifier is unique on the results. Duplicated identifiers could cause that some records are not showed, so be careful.

If the query is not sorted by the identifier, the cursor pagination might not work as expected.

Auto-detecting Identifier

If no identifier is defined, the cursor will try to figure it out by itself. First, it will check if there's any orderBy clause. If there is, it will take the first column that is sorted and will use that. If there is not any orderBy clause, it will check if it's an Eloquent model, and will use it's primaryKey (by default it's 'id'). And if it's not an Eloquent Model, it'll use id.

Example
// Will use Booking's primaryKey
Bookings::cursorPaginate(10);
// Will use hardcoded 'id'
DB::table('bookings')->cursorPaginate(10);
// Will use 'created_by'
Bookings::orderBy('created_by', 'asc')
    ->cursorPaginate(10);

Customizing Identifier

Just define the identifier option

// Will use _id, ignoring everything else.
Bookings::cursorPaginate(10, ['*'], [
    'identifier'      => '_id'
]);

Date cursors

By default, the identifier is the model's primaryKey (or id in case it's not an Eloquent model) so there are no duplicates, but you can adjust this by passing a custom identifier option. In case that specified identifier is casted to date or datetime on Eloquent's $casts property, the paginator will convert it into unix timestamp for you.

You can also specify it manually by adding a [ 'date_identifier' => true ] option.

Example

Using Eloquent (make sure Booking Model has protected $casts = ['datetime' => 'datetime'];)

// It will autodetect 'datetime' as identifier,
//   and will detect it's casted to datetime.
Bookings::orderBy('datetime', 'asc')
    ->cursorPaginate(10);

Using Query

// It will autodetect 'datetime' as identifier,
//   but since there is no model, you'll have to
//   specify the 'date_identifier' option to `true`
DB::table('bookings')
    ->orderBy('datetime', 'asc')
    ->cursorPaginate(10, ['*'], [
        'date_identifier' => true
    ]);

Inherits Laravel's Pagination

You should know that CursorPaginator inherits from Laravel's AbstractPaginator, so methods like withPath, appends, fragment are available, so you can use those to build the url, but you should know that the default url creation of this package includes query params that you might already have.

Displaying Pagination Results

Converting to JSON

A basic return will transform the paginator to JSON and will have a result like this:

Route::get('api/v1', function () {
    return App\User::cursorPaginate();
});

Calling api/v1 will output:

{
   "path": "api/v1?",
   "previous_cursor": "10",
   "next_cursor": "3",
   "per_page": 3,
   "next_page_url": "api/v1?next_cursor=3",
   "prev_page_url": "api/v1?previous_cursor=1",
   "data": [
        {}
   ]
}

Using a Resource Collection

By default, Laravel's API Resources when using them as collections, they will output a paginator's metadata into links and meta.

{
   "data":[
        {}
   ],
   "links": {
       "first": null,
       "last": null,
       "prev": "api/v1?previous_cursor=1",
       "next": "api/v1?next_cursor=3"
   },
   "meta": {
       "path": "api/v1?",
       "previous_cursor": "1",
       "next_cursor": "3",
       "per_page": 3
   }
}

Understanding the previous cursor

It's important to clarify that the previous cursor does NOT work like the next one. The next cursor makes the paginator return the elements that follow that cursor.

So in the case of the next cursor being 10, that pagination should return next + 1, next + 2, next + 3. So that works as expected.

The previous cursor though it's not prev - 1, prev - 2, prev - 3. No. It does not return the adjacent elements, or the 'context' for that cursor. What it does instead is pretty interesting:

Let's assume we do a simple fetch and it returns elements [10, 9, 8]. If we do a prev=10, it will return empty, cause those are the first elements. So if we now add 5 more to that: from the 15 to the 11, and we do prev=10 again, the result will be [15, 14, 13]. NOT [13, 12 ,11].

That's because previous works like a filter. It shows the first 'per_page' items that are before that cursor. The order is the same as in the next cursor, so it fetches the latest ones.

The same logic can be used when combining cursors: next=13&prev=10 will output the missing two elements: [12, 11], so you can complete the list without fetching any extra items.

Customizing per page results

With this plugin you can specify the perPage number by many ways.

You can set the cursor_pagination.per_page config, so if you don't send any parameters, it will use that as the default.

You can also use an array. The purpose of the array declaration is to separate whenever it's a previous cursor, or a next / default cursor. This way, when you do a 'refresh', you can fetch more results than if you are simply scrolling.

To configure that, set $perPage = [15, 5]. That way it'll fetch 15 when you do a previous_cursor, and 5 when you do a normal fetch or a next_cursor.

Customizing param names

On the config, change identifier_name to change the word cursor. Examples: cursor, id, pointer, etc.

Change navigation_names to change the words ['previous', 'next']. Examples: ['before', 'after'] , ['min', 'max']

Transforming the result:

Edit transform_name to format the string differently:

  • For previousCursor use 'camel_case'
  • For previous-cursor use 'kebab_case'.
  • For previous_cursor use 'snake_case'.

Using null defaults to camelCase.

API Docs

Paginator custom options

new CursorPaginator(array|collection $items, array|int $perPage, array options = [
    // Attribute used for choosing the cursor. Used primaryKey on Eloquent Models as default.
    'identifier'       => 'id',
    'identifier_alias' => 'id',
    'date_identifier'  => false,
    'path'             => request()->path(),
]);

(The items must have a $item->{$identifier} property.)

Eloquent Builder and Query Builder's macro:

cursorPaginate(array|int $perPage, array $cols = ['*'], array options = []): CursorPaginator;

Paginator methods

$resutls->hasMorePages(): bool;
$results->nextCursor(): string|null;
$results->prevCursor(): string|null;
$results->previousPageUrl(): string|null;
$results->nextPageUrl(): string|null;
$results->url(['next' => 1]): string;
$results->count(): int;

Note: all cursors are casted as strings.

Identifier Alias

Sometimes we need to use the paginator on a JOIN query. This might have duplicated columns, so we have to specify one for the sorting. Since Mysql doesn't allow us to use a selector alias on a WHERE condition, we have to use the table.column name instead.

$following = $user->following()
    ->orderBy('follows.created_at', 'desc')
    ->cursorPaginate(10, ['*'], [
        'date_identifier' => true,
        'identifier' => 'follows.created_at',
        'identifier_alias' => 'created_at',
    ]);

By default, the package will guess that the identifier_alias is the column name you specify, so if you order by follows.created_at, it will try to use the models created_at as identifier (by using the alias).

Testing

Run the tests with:

vendor/bin/phpunit

Credits

License

The MIT License (MIT). Please see License File for more information.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].