├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Facade │ └── ApiConsumer.php ├── Provider │ └── LaravelServiceProvider.php └── Router.php └── tests ├── Facade └── ApiConsumerTest.php ├── Provider └── LaravelServiceProviderTest.php └── RouterTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | src_dir: src 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | composer.phar 3 | composer.lock 4 | vendor 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | install: 3 | composer install 4 | php: 5 | - 7.2 6 | - 7.3 7 | - 7.4 8 | 9 | script: 10 | - mkdir -p build/logs 11 | - php vendor/bin/phpunit -c phpunit.xml 12 | 13 | after_script: 14 | - php vendor/bin/coveralls -v 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Internal API Consumption 2 | 3 | [![Build Status](https://travis-ci.org/esbenp/laravel-api-consumer.svg)](https://travis-ci.org/esbenp/laravel-api-consumer) [![Coverage Status](https://coveralls.io/repos/esbenp/laravel-api-consumer/badge.svg)](https://coveralls.io/r/esbenp/laravel-api-consumer) 4 | 5 | ## Installation 6 | 7 | ```bash 8 | composer require optimus/api-consumer 0.2.* 9 | ``` 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "optimus/api-consumer", 3 | "description": "API consumer", 4 | "require": { 5 | "laravel/framework": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0" 6 | }, 7 | "require-dev": { 8 | "phpunit/phpunit": "~8.0", 9 | "orchestra/testbench": "4.*", 10 | "mockery/mockery": "1.3.*", 11 | "php-coveralls/php-coveralls": "2.2.*" 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "Optimus\\ApiConsumer\\": "src/" 16 | } 17 | }, 18 | "minimum-stability": "dev", 19 | "prefer-stable": true, 20 | "license": "Apache", 21 | "authors": [ 22 | { 23 | "name": "Esben Petersen", 24 | "email": "esbenspetersen@gmail.com" 25 | } 26 | ], 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "Optimus\\ApiConsumer\\Provider\\LaravelServiceProvider" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | ./src/ 17 | 18 | 19 | 20 | 21 | ./tests/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Facade/ApiConsumer.php: -------------------------------------------------------------------------------- 1 | app->singleton('apiconsumer', function(){ 18 | $app = app(); 19 | 20 | return new Router($app, $app['request'], $app['router']); 21 | }); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Router.php: -------------------------------------------------------------------------------- 1 | app = $app; 29 | $this->request = $request; 30 | $this->router = $router; 31 | } 32 | 33 | /** 34 | * @param string $uri 35 | * @param array $data 36 | * @param array $headers 37 | * @param string $content 38 | * @return \Illuminate\Http\Response 39 | */ 40 | public function get() 41 | { 42 | return $this->quickCall('GET', func_get_args()); 43 | } 44 | 45 | /** 46 | * @param string $uri 47 | * @param array $data 48 | * @param array $headers 49 | * @param string $content 50 | * @return \Illuminate\Http\Response 51 | */ 52 | public function post() 53 | { 54 | return $this->quickCall('POST', func_get_args()); 55 | } 56 | 57 | /** 58 | * @param string $uri 59 | * @param array $data 60 | * @param array $headers 61 | * @param string $content 62 | * @return \Illuminate\Http\Response 63 | */ 64 | public function put() 65 | { 66 | return $this->quickCall('PUT', func_get_args()); 67 | } 68 | 69 | /** 70 | * @param string $uri 71 | * @param array $data 72 | * @param array $headers 73 | * @param string $content 74 | * @return \Illuminate\Http\Response 75 | */ 76 | public function delete() 77 | { 78 | return $this->quickCall('DELETE', func_get_args()); 79 | } 80 | 81 | /** 82 | * @param array $requests An array of requests 83 | * @return array 84 | */ 85 | public function batchRequest(array $requests) 86 | { 87 | foreach($requests as $i => $request){ 88 | $requests[$i] = call_user_func_array([$this, 'singleRequest'], $request); 89 | } 90 | 91 | return $requests; 92 | } 93 | 94 | /** 95 | * @param string $method 96 | * @param array $args 97 | * @return \Illuminate\Http\Response 98 | */ 99 | public function quickCall($method, array $args) 100 | { 101 | array_unshift($args, $method); 102 | return call_user_func_array([$this, "singleRequest"], $args); 103 | } 104 | 105 | /** 106 | * @param string $method 107 | * @param string $uri 108 | * @param array $data 109 | * @param array $headers 110 | * @param string $content 111 | * @return \Illuminate\Http\Response 112 | */ 113 | public function singleRequest($method, $uri, array $data = [], array $headers = [], $content = null) 114 | { 115 | // Save the current request so we can reset the router back to it 116 | // after we've completed our internal request. 117 | $currentRequest = $this->request->instance()->duplicate(); 118 | 119 | $headers = $this->overrideHeaders($currentRequest->server->getHeaders(), $headers); 120 | 121 | if ($this->disableMiddleware) { 122 | $this->app->instance('middleware.disable', true); 123 | } 124 | 125 | $response = $this->request($method, $uri, $data, $headers, $content); 126 | 127 | if ($this->disableMiddleware) { 128 | $this->app->instance('middleware.disable', false); 129 | } 130 | 131 | // Once the request has completed we reset the currentRequest of the router 132 | // to match the original request. 133 | $this->request->instance()->initialize( 134 | $currentRequest->query->all(), 135 | $currentRequest->request->all(), 136 | $currentRequest->attributes->all(), 137 | $currentRequest->cookies->all(), 138 | $currentRequest->files->all(), 139 | $currentRequest->server->all(), 140 | $currentRequest->content 141 | ); 142 | 143 | return $response; 144 | } 145 | 146 | private function overrideHeaders(array $default, array $headers) 147 | { 148 | $headers = $this->transformHeadersToUppercaseUnderscoreType($headers); 149 | return array_merge($default, $headers); 150 | } 151 | 152 | public function enableMiddleware() 153 | { 154 | $this->disableMiddleware = false; 155 | } 156 | 157 | public function disableMiddleware() 158 | { 159 | $this->disableMiddleware = true; 160 | } 161 | 162 | /** 163 | * @param string $method 164 | * @param string $uri 165 | * @param array $data 166 | * @param array $headers 167 | * @param string $content 168 | * @return \Illuminate\Http\Response 169 | */ 170 | private function request($method, $uri, array $data = [], array $headers = [], $content = null) 171 | { 172 | // Create a new request object for the internal request 173 | $request = $this->createRequest($method, $uri, $data, $headers, $content); 174 | 175 | // Handle the request in the kernel and prepare a response 176 | $response = $this->router->prepareResponse($request, $this->app->handle($request)); 177 | 178 | return $response; 179 | } 180 | 181 | /** 182 | * @param string $method 183 | * @param string $uri 184 | * @param array $data 185 | * @param array $headers 186 | * @param string $content 187 | * @return \Illuminate\Http\Request 188 | */ 189 | private function createRequest($method, $uri, array $data = [], array $headers = [], $content = null) 190 | { 191 | $server = $this->transformHeadersToServerVariables($headers); 192 | 193 | return $this->request->create($uri, $method, $data, [], [], $server, $content); 194 | } 195 | 196 | private function transformHeadersToUppercaseUnderscoreType($headers) 197 | { 198 | $transformed = []; 199 | 200 | foreach($headers as $headerType => $headerValue) { 201 | $headerType = strtoupper(str_replace('-', '_', $headerType)); 202 | 203 | $transformed[$headerType] = $headerValue; 204 | } 205 | 206 | return $transformed; 207 | } 208 | 209 | /** 210 | * https://github.com/symfony/symfony/issues/5074 211 | * 212 | * @param array $headers 213 | * @return array 214 | */ 215 | private function transformHeadersToServerVariables($headers) 216 | { 217 | $server = []; 218 | 219 | foreach($headers as $headerType => $headerValue){ 220 | $headerType = 'HTTP_' . $headerType; 221 | 222 | $server[$headerType] = $headerValue; 223 | } 224 | 225 | return $server; 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /tests/Facade/ApiConsumerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('apiconsumer', $facade->getAccessor()); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /tests/Provider/LaravelServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('singleton')->with( 12 | 'apiconsumer', 13 | m::type('Closure') 14 | ); 15 | 16 | $provider = $this->app->make('Optimus\ApiConsumer\Provider\LaravelServiceProvider', [ 17 | 'app' => $appMock 18 | ]); 19 | 20 | $this->assertNull($provider->register()); 21 | $provider->boot(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /tests/RouterTest.php: -------------------------------------------------------------------------------- 1 | appMock = m::mock("Illuminate\Foundation\Application"); 20 | $this->requestMock = m::mock("Illuminate\Http\Request"); 21 | $this->routerMock = m::mock("Illuminate\Routing\Router"); 22 | } 23 | 24 | public function tearDown() : void 25 | { 26 | parent::tearDown(); 27 | 28 | m::close(); 29 | } 30 | 31 | public function testThatQuickCallCorrectlyCallsUnderlyingMethod() 32 | { 33 | $mock = m::mock(self::classPath . '[singleRequest]', [ 34 | $this->appMock, 35 | $this->requestMock, 36 | $this->routerMock 37 | ]); 38 | 39 | $mock->shouldReceive('singleRequest')->times(4)->withArgs([ 40 | m::anyOf('GET', 'POST', 'PUT', 'DELETE'), 41 | '/endpoint', 42 | ['data'] 43 | ]); 44 | 45 | $mock->get('/endpoint', ['data']); 46 | $mock->post('/endpoint', ['data']); 47 | $mock->put('/endpoint', ['data']); 48 | $mock->delete('/endpoint', ['data']); 49 | } 50 | 51 | public function testThatBatchRequestWillCallSingleRequest() 52 | { 53 | $mock = m::mock(self::classPath . '[singleRequest]', [ 54 | $this->appMock, 55 | $this->requestMock, 56 | $this->routerMock 57 | ]); 58 | 59 | $mock->shouldReceive('singleRequest')->times(2)->withArgs([ 60 | m::anyOf('GET', 'POST'), '/endpoint', ['data'] 61 | ]); 62 | 63 | $mock->batchRequest([ 64 | ['GET', '/endpoint', ['data']], 65 | ['POST', '/endpoint', ['data']] 66 | ]); 67 | } 68 | 69 | public function testThatQuickCallCorrectlyCallsSingleRequest() 70 | { 71 | $mock = m::mock(self::classPath . '[singleRequest]', [ 72 | $this->appMock, 73 | $this->requestMock, 74 | $this->routerMock 75 | ]); 76 | 77 | $mock->shouldReceive('singleRequest')->times(2)->withArgs([ 78 | m::anyOf('GET', 'POST'), 79 | '/endpoint', 80 | ['data'], 81 | ['server'], 82 | 'content' 83 | ]); 84 | 85 | $mock->quickCall('GET', [ 86 | '/endpoint', 87 | ['data'], 88 | ['server'], 89 | 'content' 90 | ]); 91 | 92 | $mock->quickCall('GET', [ 93 | '/endpoint', 94 | ['data'], 95 | ['server'], 96 | 'content' 97 | ]); 98 | } 99 | 100 | public function testThatRequestIsMadeCorrectlyAndThatHeadersAreCorrectlySet() 101 | { 102 | $routerMock = m::mock('Illuminate\Routing\Router'); 103 | 104 | $routerMock->shouldReceive('prepareResponse')->times(1)->with( 105 | m::on(function($request){ 106 | $this->assertEquals("content", $request->getContent()); 107 | $this->assertEquals(['data'], $request->query->all()); 108 | $this->assertEquals('/endpoint?0=data', $request->server->get('REQUEST_URI')); 109 | $this->assertTrue(array_key_exists('x-requested-with', $request->headers->all())); 110 | return true; 111 | }), 112 | m::any() 113 | ); 114 | 115 | $appMock = m::mock('Illuminate\Foundation\Application'); 116 | 117 | $appMock->shouldReceive('handle')->times(1)->with( 118 | m::type('Illuminate\Http\Request') 119 | ); 120 | 121 | $class = self::classPath; 122 | $router = new $class($appMock, $this->app['request'], $routerMock); 123 | 124 | $router->get('/endpoint', ['data'], [ 125 | 'X-Requested-With' => 'XMLHttpRequest' 126 | ], "content"); 127 | 128 | 129 | } 130 | 131 | } 132 | --------------------------------------------------------------------------------