├── .gitignore ├── composer.json ├── config └── lazy_rest.php ├── phpunit.xml ├── readme.md ├── src ├── Facades │ └── LazyRestFacade.php ├── LaravelLazyRest.php └── LazyRestServiceProvider.php └── tests └── Unit └── LaravelLazyRestTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | .phpunit.result.cache 4 | composer.lock -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robertogallea/laravel-lazy-rest", 3 | "description": "Package for trasparently loading paginated rest resources into lazy collections", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "roberto.gallea", 9 | "email": "write@robertogallea.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.2.5", 14 | "illuminate/support": "~7", 15 | "guzzlehttp/guzzle": "^6.4" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~8.5", 19 | "mockery/mockery": "^1.1", 20 | "orchestra/testbench": "~5", 21 | "sempro/phpunit-pretty-print": "^1.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "robertogallea\\LaravelLazyRest\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "robertogallea\\LaravelLazyRest\\Tests\\": "tests" 31 | } 32 | }, 33 | "scripts": { 34 | "test": "phpunit" 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "robertogallea\\LaravelLazyRest\\LazyRestServiceProvider" 40 | ], 41 | "aliases": { 42 | "LazyRest": "robertogallea\\LaravelLazyRest\\Facades\\LazyRestFacade" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/lazy_rest.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'next_page_url' => 'next_page_url', 7 | 'data' => 'data', 8 | ], 9 | 10 | 'timeout' => 5.0, 11 | 12 | ]; -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | ./tests/Unit 17 | 18 | 19 | 20 | 21 | ./app 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel-Lazy-Rest 2 | 3 | This package provides loading of RESTful paginated resources using Laravel LazyCollection capabilities. In this way, no complicate mechanisms for on-demand paginated data handling is required because the package manages all the complexity transparently for you, and only if required. 4 | 5 | ```php 6 | $paginatedEndpoint = 'http://api.with-15-elements-per-page'; 7 | 8 | $collection = \LazyRest::load($paginatedEndpoint); 9 | 10 | // Loads only first page 11 | dump($collection->skip(10)->first()); 12 | 13 | // loads all the pages 14 | dump($collection->last()); 15 | ``` 16 | 17 | ## 1. Installation 18 | To install the package, run the command 19 | 20 | `composer require robertogallea/laravel-lazy-rest` 21 | 22 | ServiceProvider and Facade alias included in the package are autoloaded automatically using Laravel auto-discovery 23 | service. 24 | 25 | ## 2. Configuration 26 | By default, the package is configured to search for `data` field in the json response and `next_page_url` to detect how 27 | to fetch the next page of results. However you could easily change this behavior by overriding default configuration: 28 | 29 | 1. Publish config: 30 | `php artisan vendor:publish --provider="robertogallea\LaravelLazyRest\LazyRestServiceProvider" --tag=config` 31 | 32 | 2. Edit `config/lazy-rest.php` 33 | ```php 34 | 'fields' => [ 35 | 'next_page_url' => 'next_page_url', 36 | 'data' => 'data', 37 | 38 | 'timeout' => 5.0, 39 | ]; 40 | ``` 41 | if your data are in the root of the response use `_` character as field name (even though this means your endpoint 42 | doesn't provide paginated data. 43 | 44 | # 3. Issues, Questions and Pull Requests 45 | You can report issues and ask questions in the 46 | [issues section](https://github.com/robertogallea/Laravel-Lazy-Rest/issues). 47 | Please start your issue with ISSUE: and your question 48 | with QUESTION: 49 | 50 | If you have a question, check the closed issues first. Over time, I've been able to answer quite a few. 51 | 52 | To submit a Pull Request, please fork this repository, create a new branch and commit your new/updated code in there. 53 | Then open a Pull Request from your new branch. Refer to 54 | [this guide](https://help.github.com/articles/about-pull-requests/) for more info. 55 | -------------------------------------------------------------------------------- /src/Facades/LazyRestFacade.php: -------------------------------------------------------------------------------- 1 | getNextPage($nextPage, $options); 32 | 33 | $data = $this->offsetKeys($data, $count); 34 | 35 | $count+=sizeof($data); 36 | 37 | yield from $data; 38 | } 39 | }); 40 | } 41 | 42 | private function getNextPage(string $nextPage, array $options): array 43 | { 44 | $response = Http::withOptions($options)->get($nextPage); 45 | 46 | $data = $response->json(); 47 | 48 | $nextPage = $data[config('lazy_rest.fields.next_page_url')] ?? null; 49 | 50 | if (config('lazy_rest.fields.data') == '_') { 51 | return array($data, $nextPage); 52 | } 53 | 54 | return array($data[config('lazy_rest.fields.data')], $nextPage); 55 | } 56 | 57 | private function offsetKeys(array $data, int $count) 58 | { 59 | $newData = []; 60 | 61 | foreach ($data as $key => $value) { 62 | $newData[$key + $count] = $value; 63 | } 64 | 65 | return $newData; 66 | } 67 | } -------------------------------------------------------------------------------- /src/LazyRestServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/lazy_rest.php', 'lazy_rest'); 18 | 19 | $this->app->singleton('lazyrest', function($app) { 20 | return new LaravelLazyRest(); 21 | }); 22 | } 23 | 24 | 25 | /** 26 | * Boots the service provider 27 | */ 28 | public function boot() 29 | { 30 | if ($this->app->runningInConsole()) { 31 | $this->bootForConsole(); 32 | } 33 | } 34 | 35 | public function bootForConsole() 36 | { 37 | $this->publishes([ 38 | __DIR__ . '/../config/lazy_rest.php' => config_path('lazy_rest.php') 39 | ], 'config'); 40 | } 41 | } -------------------------------------------------------------------------------- /tests/Unit/LaravelLazyRestTest.php: -------------------------------------------------------------------------------- 1 | set('lazy_rest.fields.data', $dataField); 29 | 30 | $mockSequence = Http::sequence(); 31 | 32 | foreach ($mockResponses as $mockResponse) { 33 | $responses[] = $mockSequence->push($mockResponse); 34 | } 35 | 36 | Http::fake([ 37 | 'http://test-endpoint.it/api/bla*' => $mockSequence 38 | ]); 39 | 40 | $lazyRest = new LaravelLazyRest(); 41 | 42 | $collection = $lazyRest->load('http://test-endpoint.it/api/bla'); 43 | 44 | $this->assertInstanceOf(LazyCollection::class, $collection); 45 | $this->assertCount($count, $collection->all()); 46 | } 47 | 48 | /** 49 | * @test 50 | * @dataProvider mockHandlers 51 | */ 52 | public function it_can_add_options($mockResponses, $dataField, $count) 53 | { 54 | config()->set('lazy_rest.fields.data', $dataField); 55 | 56 | $mockSequence = Http::sequence(); 57 | 58 | foreach ($mockResponses as $mockResponse) { 59 | $responses[] = $mockSequence->push($mockResponse); 60 | } 61 | 62 | Http::fake([ 63 | 'http://test-endpoint.it/api/bla*' => $mockSequence 64 | ]); 65 | 66 | $lazyRest = new LaravelLazyRest(); 67 | 68 | $collection = $lazyRest->load('http://test-endpoint.it/api/bla', [ 69 | 'headers' => [ 70 | 'User-Agent' => 'testing/1.0', 71 | 'Accept' => 'application/json', 72 | ], 73 | 'query' => ['foo' => 'bar'], 74 | ]); 75 | 76 | $this->assertCount($count, $collection->all()); 77 | 78 | Http::assertSent(function ($request) { 79 | $this->assertTrue($request->hasHeader('User-Agent', 'testing/1.0')); 80 | $this->assertTrue($request->hasHeader('Accept', 'application/json')); 81 | $this->assertEquals('http://test-endpoint.it/api/bla?foo=bar', $request->url()); 82 | return true; 83 | }); 84 | } 85 | 86 | public function mockHandlers() 87 | { 88 | return [ 89 | [$this->getSinglePageMockHandler(), 'data', 3], 90 | [$this->getMultiPageMockHandler(), 'data', 6], 91 | [$this->getRootDataMockHandler(), '_', 2] 92 | ]; 93 | } 94 | 95 | /** @test */ 96 | public function it_can_use_facade() 97 | { 98 | Http::fake([ 99 | 'http://test-endpoint.it/api/bla*' => Http::sequence() 100 | ->push($this->getSinglePageMockHandler()[0]) 101 | ]); 102 | 103 | $collection = \LazyRest::load('http://test-endpoint.it/api/bla'); 104 | 105 | $this->assertInstanceOf(LazyCollection::class, $collection); 106 | $this->assertEquals(3, $collection->count()); 107 | } 108 | 109 | private function getSinglePageMockHandler() 110 | { 111 | return [ 112 | [ 113 | 'next_page_url' => null, 114 | 'data' => [ 115 | ['id' => 1, 'text' => 'abc'], 116 | ['id' => 2, 'text' => 'def'], 117 | ['id' => 3, 'text' => 'ghi'], 118 | ] 119 | ] 120 | ]; 121 | } 122 | 123 | private function getMultiPageMockHandler() 124 | { 125 | return [ 126 | [ 127 | 'next_page_url' => 'http://test-endpoint.it/api/bla?page=2', 128 | 'data' => [ 129 | ['id' => 1, 'text' => 'abc'], 130 | ['id' => 2, 'text' => 'def'], 131 | ['id' => 3, 'text' => 'ghi'], 132 | ] 133 | ], 134 | [ 135 | 'next_page_url' => null, 136 | 'data' => [ 137 | ['id' => 4, 'text' => 'jkl'], 138 | ['id' => 5, 'text' => 'mno'], 139 | ['id' => 6, 'text' => 'pqr'], 140 | ] 141 | ] 142 | ]; 143 | } 144 | 145 | protected function getRootDataMockHandler() 146 | { 147 | return [ 148 | [ 149 | ['a' => 1], 150 | ['a' => 2] 151 | ] 152 | ]; 153 | } 154 | 155 | protected function getPackageProviders($app) 156 | { 157 | return [ 158 | 'robertogallea\LaravelLazyRest\LazyRestServiceProvider' 159 | ]; 160 | } 161 | 162 | protected function getPackageAliases($app) 163 | { 164 | return [ 165 | 'LazyRest' => LazyRestFacade::class 166 | ]; 167 | } 168 | } --------------------------------------------------------------------------------