Coder Social home page Coder Social logo

Pagination about laravel-json-api HOT 14 CLOSED

cloudcreativity avatar cloudcreativity commented on July 20, 2024
Pagination

from laravel-json-api.

Comments (14)

timnolte avatar timnolte commented on July 20, 2024 1

@lindyhopchris haven't had a chance to check out the branch yet. I ended up implementing my own quick-and-dirty handling for pagination through the use of a parent controller, a custom method for building the pagination links and using the pagination object to get at the array of models. I'm not 100% sure I'm keen on the custom Paginator, I think I'd much rather see the encoder handle the Eloquent paginator and take that as an indicator to automatically append Link & Meta data to anything passes in the content() call. If it didn't automatically append the Link & Meta data by default then I'd would go with a protected parameter that could be set to indicate whether that should happen.

from laravel-json-api.

lindyhopchris avatar lindyhopchris commented on July 20, 2024

@samsebastian

Hi! The neomerx/json-api package that this package is built on top tries to cast several of the Laravel database builder results to objects for encoding, rather than iterating over them. The paginator one is an example.

Luckily the solution is easy - just convert the LengthAwarePaginator to an array before passing it out of the controller to the reply encoding.

from laravel-json-api.

 avatar commented on July 20, 2024

Awesome thanks!

from laravel-json-api.

lindyhopchris avatar lindyhopchris commented on July 20, 2024

Note to self... for the future change this line:

https://github.com/cloudcreativity/laravel-json-api/blob/master/src/Http/Responses/ResponsesHelper.php#L101

So that it also does casting of Laravel builder objects.

from laravel-json-api.

 avatar commented on July 20, 2024

Just saw your note to self above, that helped me massively - until you are able to implement, I've had to temporarily extend the ResponsesHelper to add in use Illuminate\Database\Eloquent\Collection as EloquentCollection; and check for that at if ($data instanceof Collection || $data instanceof EloquentCollection)

I might be doing something wrong here as well, but I've found I need to do the same check on the ->relationship() method. I pass in an Eloquent Collection and it triggers the same Schema not registered for.. error.

from laravel-json-api.

lindyhopchris avatar lindyhopchris commented on July 20, 2024

Ah, great thanks for letting me know about the relationship method as well - will fix that at the same time.

Not sure when I'll be able to get round to doing the fix as I have a massive deadline on my current project at the beginning of Feb. I'll try to get to it soon, but if not I'll fix it very early Feb.

from laravel-json-api.

 avatar commented on July 20, 2024

Good luck on the deadline!

Just on the pagination note, you might end up implementing it a completely different way but this is what I ended up doing to handle the response so far which might help anyone looking in the short term.

Looking at the flow it seems like it would be might be cleaner to check for LengthAwarePaginator in the ResponsesHelper and append the links and meta there if it detects that it needs it? Or maybe even a separate method like paginatedContent()?

Additionally, in my project I eventually want to spin off the validation etc into a quasi-traditional Laravel way, where I have a separate XyzRequest.php file for each endpoint in the App\Http\Requests namespace and typehint it into the controller, rather than doing the validation in the controller.

Pagination

  1. Creating a new Pagination Service Provider
<?php

namespace App\Providers;

use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;

class JsonApiPaginationServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public
    function register()
    {

        Paginator::currentPathResolver(function ()
        {
            return $this->app['request']->url();
        });

        Paginator::currentPageResolver(function ()
        {
            $page = $this->app['request']->input('page.number');

            if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int)$page >= 1)
            {
                return $page;
            }

            return 1;
        });

    }
}
  1. Overriding the default Pagination Service provider in config/app.php
'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
                ...
        Illuminate\Mail\MailServiceProvider::class,
        //Illuminate\Pagination\PaginationServiceProvider::class, (commented this out)
        Illuminate\Pipeline\PipelineServiceProvider::class,
        ...

        /*
         * JSONAPI Service Providers (Must be added before Route Service Provider)
         */
        CloudCreativity\JsonApi\ServiceProvider::class,
        App\Providers\JsonApiPaginationServiceProvider::class, // (added this)
  1. Extending the JsonApiController (my app controllers extend this..)
<?php

namespace App\Http\Controllers;

use CloudCreativity\JsonApi\Http\Controllers\JsonApiController;

use Neomerx\JsonApi\Schema\Link;

abstract
class ApiController extends JsonApiController
{

    protected $allowedPagingParameters = ['number', 'size'];

    protected
    function getPaginationParameters()
    {
        $params = $this->getParameters()->getPaginationParameters();

        return [
            'size'   => isset($params['size']) ? intval($params['size']) : 15,
            'number' => isset($params['number']) ? intval($params['number']) : 1
        ];
    }

    protected
    function generatePaginationLinks($resource, $url, $appends = [])
    {

        $page = $this->getPaginationParameters();

        $append = "";

        foreach($appends as $key => $value) {
            $append .= "&" . $key . "=" . $value;
        }

        $links = [
            Link::FIRST => new Link(sprintf('/%s?page[number]=%s&page[size]=%s%s', $url, 1, $page['size'], $append)),
            Link::NEXT  => new Link(sprintf('/%s?page[number]=%s&page[size]=%s%s', $url, $page['number'] < $resource->lastPage() ? $page['number'] + 1 : $page['number'], $page['size'], $append)),
            Link::PREV  => new Link(sprintf('/%s?page[number]=%s&page[size]=%s%s', $url, $page['number'] > 1 ? $page['number'] - 1 : $page['number'], $page['size'], $append)),
            Link::LAST  => new Link(sprintf('/%s?page[number]=%s&page[size]=%s%s', $url, $resource->lastPage(), $page['size'], $append))
        ];

        return $links;

    }

    protected
    function readRelated($resource, $relationshipName)
    {
        if (!$resource)
        {
            $this->notFound();
        }

        $resource->load($relationshipName);

        return $this
            ->reply()
            ->content($resource->{$relationshipName});
    }


    protected
    function readRelatedRelationship($resource, $relationshipName)
    {
        if (!$resource)
        {
            $this->notFound();
        }

        $resource->load($relationshipName);

        return $this
            ->reply()
            ->relationship($resource, $relationshipName, $resource->{$relationshipName});
    }
}
  1. Adding a check in the ResponsesHelper (hate doing this, way too hacky but I figure it's just temporary)
<?php

namespace CloudCreativity\JsonApi\Http\Responses;

...

use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; // Added this
use Illuminate\Pagination\LengthAwarePaginator; // Added this
....

public
    function content($data, array $links = [], $meta = null, $statusCode = Response::HTTP_OK, array $headers = [])
    {
        /** Eloquent collections do not encode properly, so we'll get all just in case it's an Eloquent collection */


        if ($data instanceof Collection || $data instanceof EloquentCollection || $data instanceof LengthAwarePaginator) // Added EloquentCollection and LengthAwarePaginator
        {
            $data = $data->all();
        }

        $content = $this
            ->getEncoder()
            ->withLinks($links)
            ->withMeta($meta)
            ->encodeData($data, $this->environment->getParameters());

        return $this->respond($statusCode, $content, $headers);
    }

  1. Then in an an app controller I need paginated
<?php

namespace App\Http\Controllers\Contact;

use App\Http\Controllers\ApiController;
use App\Http\Requests\Contact\ContactRequest;
use App\Models\Contact\Contact;


class ContactController extends ApiController
{

    /*
     * Getters
     */

    public
    function index()
    {


        $page = $this->getPaginationParameters();

        $contacts = Contact::with('codes', 'status')->paginate($page['size']);

        if (!$contacts)
        {
            $this->notFound();
        }

        $links = $this->generatePaginationLinks($contacts);

        $meta = [
            'total_pages' => $contacts->lastPage()
        ];


        return $this
            ->reply()
            ->content($contacts, $links, $meta);
    }

from laravel-json-api.

timnolte avatar timnolte commented on July 20, 2024

@samsebastian the changes you made to ResponsesHelper was that done via some sort of extends or did you actually change the code in the vendors source for laravel-json-api? I need to get this working now in my own project and would like to use your unofficial fix for now. I guess worst case would be that I would just fork this repository and add the features in my own fork and add my fork to my composer.json instead of the real package until it's been added.

from laravel-json-api.

lindyhopchris avatar lindyhopchris commented on July 20, 2024

@timnolte

I've got time to look at this Mon-Weds next week, and can see what I might add to the laravel-json-api repository. I'm not sure I'd bring all of this code in this way - need to review it.

What features from it are you planning to use?

from laravel-json-api.

timnolte avatar timnolte commented on July 20, 2024

@lindyhopchris

The biggest piece that I need is the ability to use the Laravel pagination results in my JSON API response so that my client can request additional pages. I don't need any Request magic as I can handle that via an extended controller.

from laravel-json-api.

 avatar commented on July 20, 2024

The above were really just quick hacks to test out what I ended up implementing, I had to dig way further into a lot of things such as ->getResourceObjectValidator() (inside the vendor folder) to deal with pagination and relationships that were failing validation, and creating a custom ApiRequest.php like class ApiRequest extends Request implements ValidatesWhenResolved to try and quasi-replicate the traditional Laravel request functionality (ie typehint into the controller class and auto validate, access methods like $request->all(), $request->input('foo') etc, plus because the JSONAPI packets come in with relationships separate, I need to automatically pickup 'hasOnerelationships and convert them to a new fieldfoo_idso I can do things likeBar::create($request->all())` which I wanted to still be able to use.).

As an example (not the full ApiRequest class, just a snippet, the validate() method is triggered by the ValidatesWhenResolved)

public
    function validate()
    {

        if (!$this->authorize())
        {
            throw new HttpResponseException(new JsonResponse('Authorization Exception here', 401));
        }


        if ($this->isMethod('post') || $this->isMethod('patch') || $this->isMethod('put'))
        {
            if ($this->rules() || $this->relationships())
            {
                $validator = $this
                    ->getResourceObjectValidator(
                    // the expected resource type
                        $this->resourceType,
                        // the expected id (use null for new resources)
                        $this->id(),
                        // the rules for the attributes - uses the Laravel validation implementation.
                        $this->rules(),
                        // Laravel validation messages, if you need to customise them
                        $this->validationMessages(),
                        // whether the attributes member must be present in the resource object
                        $this->required(),
                        // the rules for expected relationships
                        $this->relationships()
                    );
            }
            else
            {
                $validator = null;
            }

            try
            {
                $object = $this->getResourceObject($validator);
            }
            catch (MultiErrorException $e)
            {
                throw new HttpResponseException(new JsonResponse('Body Content Exception here', 401));
            }

            $this->resource = $object;

            if ($object->hasRelationships() && $this->relationships())
            {
                foreach ($this->relationships() as $name => $type)
                {
                    $relationship = $object->getRelationships()->get($name)->getData();

                    if (!empty($relationship))
                    {
                        if ($type == "hasOne")
                        {
                            if ($relationship->hasId())
                            {
                                $relationshipMap = $this->relationshipMap();

                                if (array_key_exists($name, $relationshipMap))
                                {
                                    $name = $relationshipMap[ $name ];

                                }

                                $this->resource->attributes->{strtolower($name) . '_id'} = $relationship->getId();

                                $this->hasOneRelationships[ $name ] = $relationship->getId();
                            }

                        }
                        elseif ($type == "hasMany")
                        {
                            foreach ($relationship as $member)
                            {
                                $this->hasManyRelationships[ $member->type ][] = $member->id;
                            }
                        }
                    }
                }
            }

            // Strip any meta data we don't want to mass assign

            $metas = ['created_at', 'updated_at', 'deleted_at'];

            foreach ($metas as $meta)
            {
                unset($this->resource->attributes->{$meta});
            }

            $this->sanitize();

        }
        else
        {
            //var_dump($this->method());
        }

        //  Should automatically do an Exception if wrong

        if ($this->isMethod('get')) {

            $this->filters = $this->getFilteringParameters();

        }


    }

And then for requests I know will be paged:

<?php namespace App\Http\Requests;

abstract
class PagedApiRequest extends ApiRequest
{

    protected $allowedPagingParameters = ['number', 'size'];


    protected $page;
    protected $defaultPageSize = 15;


    protected
    function getPaginationParameters()
    {
        $params = $this->getParameters()->getPaginationParameters();

        return [
            'size'   => isset($params['size']) ? intval($params['size']) : $this->defaultPageSize,
            'number' => isset($params['number']) ? intval($params['number']) : 1
        ];
    }



    public
    function page($key = null)
    {
        if ($key)
        {
            return isset($this->page[ $key ]) ? $this->page[ $key ] : null;
        }

        return $this->page;

    }

    public
    function validate()
    {
        parent::validate();
        $this->page = $this->getPaginationParameters();
    }


}

And then finally extending to the specific Request, in this case a "Note" request..

<?php

namespace App\Http\Requests\Note;

use App\Http\Requests\PagedApiRequest;

class NoteRequest extends PagedApiRequest
{

    protected $resourceType = 'notes';
    protected $allowedFilteringParameters = ['contact_id', 'user_id'];

    public
    function rules()
    {

        return [
            'content' => 'string'
        ];
    }

    public
    function relationships()
    {
        return [
            'contact'   => 'hasOne',
            'user'      => 'hasOne',
            'note-type' => 'hasOne',
        ];
    }


    public
    function authorize()
    {

        return true;
    }


}

One thing I did have to change in the vendor folder was this src\Validator\Resource\IlluminateResourceValidator.php

I had no idea what I was doing here but I had to change as the relationships kept crashing when I validated. (Relationship definitions come from the extended ApiRequest class)

public
    function __construct(
        $expectedType,
        $expectedId = null,
        array $attributesRules = [],
        array $attributesValidationMessages = [],
        $attributesRequired = true,
        array $relationshipsRules = [],
        $relationshipsRequired = false
    )
    {
        $this->expectedType      = $expectedType;
        $this->expectedId        = $expectedId;
        $this->attributes        =
            new RulesValidator($attributesRules, $attributesValidationMessages, $attributesRequired);
        $relationshipsValidators = [];
        foreach ($relationshipsRules as $relationship => $type)
        {
            switch ($type)
            {
                case "hasOne":
                    $relationshipsValidators[ $relationship ] = new HasOneValidator(str_plural($relationship));
                    break;
                case "hasMany":
                default:
                    $relationshipsValidators[ $relationship ] = new HasManyValidator($relationship);
                    break;;
            }
        }
        $this->relationships = new RelationshipsValidator($relationshipsValidators);
    }

I wouldn't suggest looking at the stuff above (or even anything I ended up stitching together) as anything other than a potential starting point, because I was in a rush and trying to rapidly figure out what was going on within this package and two levels down in the dependencies, so I'm expecting it to just get me through until a real solution is released.

from laravel-json-api.

lindyhopchris avatar lindyhopchris commented on July 20, 2024

Hi everyone!

I have a working version of pagination on the feature/pagination branch.

In your controller, apply the following trait:
CloudCreativity\JsonApi\Http\Controllers\PaginatedTrait

Then in your controller method you can do:

/** @var CloudCreativity\JsonApi\Pagination\Paginator */
$paginator = $this->getPaginator();

$query = Comment::paginate($paginated->getPerPage(25, 25));

return $this
    ->reply()
    ->content(
        $query, 
        $paginator->getLinks($query), 
        $paginator->getMeta($query)
    );

The syntax of the getPerPage() function is:
getPerPage($default = 15, $max = null)

I.e. a default if the client hasn't provided a page[number] parameter. The $max is if you want to limit the maximum they can request per page (use null to indicate no limit).

I've used page[number] and page[size] as the two query params. I've intentionally used single words because different applications may adopt different policies on whether they are e.g. hyphenating or underscoring their keys.

The getPaginator() method from the PaginatedTrait allows you to override what the keys are.

The slight complication is on constructing the links that should be returned from the Paginator::getLinks() method. What I've done is created a links class that reconstructs the url from the page, filter and sort parameters received from the client - as my view was that those three are going to affect what you're paging over.

I'd be interested on what people think about this and the feature/pagination branch. I've intentionally not merged it yet because feedback would be good. Obviously let me know if you have any questions!

from laravel-json-api.

lindyhopchris avatar lindyhopchris commented on July 20, 2024

@timnolte thanks, I think that sounds like a better approach. I'll have another go at re-factoring this.

from laravel-json-api.

lindyhopchris avatar lindyhopchris commented on July 20, 2024

Closing this as the v0.4 has fully support for pagination. Pagination meta and links are automatically created if you return a Laravel paginator result.

from laravel-json-api.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.