├── tests ├── fixtures │ ├── basic-get.in │ ├── basic-get.out │ ├── basic-post.in │ ├── ignore-location-flag.in │ ├── ignore-location-flag.out │ ├── max-timeout.in │ ├── missing-url-scheme.in │ ├── connect-timeout.in │ ├── post-json.in │ ├── put-with-data.in │ ├── with-cert-path-only.in │ ├── max-timeout.out │ ├── accept-json-response.in │ ├── accept-json-response.out │ ├── connect-timeout.out │ ├── post-json.out │ ├── with-cert-path-and-password.in │ ├── with-collapsable-headers.in │ ├── with-compressed-option.out │ ├── with-headers.in │ ├── with-insecure-k-option.in │ ├── with-cert-and-key.in │ ├── with-collapsable-headers.out │ ├── with-insecure-option.in │ ├── with-compressed-option.in │ ├── post-with-data.in │ ├── post-with-form-data.in │ ├── request-with-form-data.in │ ├── basic-post.out │ ├── with-insecure-k-option.out │ ├── with-insecure-option.out │ ├── missing-url-scheme.out │ ├── request-with-data.in │ ├── raw-data-mixed.in │ ├── request-with-form-data.out │ ├── with-cert-path-only.out │ ├── put-with-data.out │ ├── request-with-data.out │ ├── with-headers.out │ ├── raw-data-mixed.out │ ├── stripe-query-params.in │ ├── with-cert-and-key.out │ ├── stripe-example.in │ ├── with-query-string.in │ ├── digital-ocean-example.out │ ├── with-raw-data.out │ ├── with-cert-path-and-password.out │ ├── digital-ocean-example.in │ ├── stripe-query-params.out │ ├── with-raw-data.in │ ├── post-with-data.out │ ├── post-with-form-data.out │ ├── mailgun-example.in │ ├── stripe-example.out │ ├── with-query-string.out │ └── mailgun-example.out ├── TestCase.php └── Feature │ └── Console │ └── Commands │ └── CurlCommandTest.php ├── .gitattributes ├── .gitignore ├── .editorconfig ├── src ├── Providers │ └── ServiceProvider.php ├── Console │ └── Commands │ │ └── CurlCommand.php ├── Support │ └── HttpCall.php └── Models │ └── Request.php ├── pint.json ├── phpunit.xml ├── README.md ├── LICENSE └── composer.json /tests/fixtures/basic-get.in: -------------------------------------------------------------------------------- 1 | curl https://laravelshift.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/basic-get.out: -------------------------------------------------------------------------------- 1 | Http::get('https://laravelshift.com'); 2 | -------------------------------------------------------------------------------- /tests/fixtures/basic-post.in: -------------------------------------------------------------------------------- 1 | curl -X POST -d 'foo=bar' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/ignore-location-flag.in: -------------------------------------------------------------------------------- 1 | curl --location https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/ignore-location-flag.out: -------------------------------------------------------------------------------- 1 | Http::get('https://example.com'); 2 | -------------------------------------------------------------------------------- /tests/fixtures/max-timeout.in: -------------------------------------------------------------------------------- 1 | curl --max-timeout 5 https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/missing-url-scheme.in: -------------------------------------------------------------------------------- 1 | curl -X POST -d 'foo=bar' example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/connect-timeout.in: -------------------------------------------------------------------------------- 1 | curl --connect-timeout 5 https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/post-json.in: -------------------------------------------------------------------------------- 1 | curl -X POST -d '{"foo":"bar"}' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/put-with-data.in: -------------------------------------------------------------------------------- 1 | curl -X PUT -d 'foo=bar&baz=qui' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-cert-path-only.in: -------------------------------------------------------------------------------- 1 | curl --cert /path/to/cert https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/max-timeout.out: -------------------------------------------------------------------------------- 1 | Http::timeout(5) 2 | ->get('https://example.com'); 3 | -------------------------------------------------------------------------------- /tests/fixtures/accept-json-response.in: -------------------------------------------------------------------------------- 1 | curl -H 'Accept: application/json' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/accept-json-response.out: -------------------------------------------------------------------------------- 1 | Http::acceptJson() 2 | ->get('https://example.com'); 3 | -------------------------------------------------------------------------------- /tests/fixtures/connect-timeout.out: -------------------------------------------------------------------------------- 1 | Http::connectTimeout(5) 2 | ->get('https://example.com'); 3 | -------------------------------------------------------------------------------- /tests/fixtures/post-json.out: -------------------------------------------------------------------------------- 1 | Http::withBody('{"foo":"bar"}') 2 | ->post('https://example.com'); 3 | -------------------------------------------------------------------------------- /tests/fixtures/with-cert-path-and-password.in: -------------------------------------------------------------------------------- 1 | curl -E /path/to/cert:password https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-collapsable-headers.in: -------------------------------------------------------------------------------- 1 | curl -H 'Accept: application/json' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-compressed-option.out: -------------------------------------------------------------------------------- 1 | Http::acceptJson() 2 | ->get('https://example.com'); 3 | -------------------------------------------------------------------------------- /tests/fixtures/with-headers.in: -------------------------------------------------------------------------------- 1 | curl -H "X-Header-One: 1" -H 'X-Header-Two: 2' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-insecure-k-option.in: -------------------------------------------------------------------------------- 1 | curl -H 'Accept: application/json' https://example.com -k 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-cert-and-key.in: -------------------------------------------------------------------------------- 1 | curl --cert /path/to/cert --key /path/to/key https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-collapsable-headers.out: -------------------------------------------------------------------------------- 1 | Http::acceptJson() 2 | ->get('https://example.com'); 3 | -------------------------------------------------------------------------------- /tests/fixtures/with-insecure-option.in: -------------------------------------------------------------------------------- 1 | curl -H 'Accept: application/json' https://example.com --insecure 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-compressed-option.in: -------------------------------------------------------------------------------- 1 | curl -H 'Accept: application/json' https://example.com --compressed 2 | -------------------------------------------------------------------------------- /tests/fixtures/post-with-data.in: -------------------------------------------------------------------------------- 1 | curl -X POST -d 'foo=bar' -d 'rho[]=1' -d 'baz=qui&rho[]=2' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/post-with-form-data.in: -------------------------------------------------------------------------------- 1 | curl -X POST -F 'foo=bar' -F 'rho[]=1' -F 'baz=qui&rho[]=2' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/request-with-form-data.in: -------------------------------------------------------------------------------- 1 | curl -H 'Content-Type: multipart/form-data' -F 'foo=bar' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/basic-post.out: -------------------------------------------------------------------------------- 1 | Http::asForm() 2 | ->post('https://example.com', [ 3 | 'foo' => 'bar', 4 | ]); 5 | -------------------------------------------------------------------------------- /tests/fixtures/with-insecure-k-option.out: -------------------------------------------------------------------------------- 1 | Http::acceptJson() 2 | ->withoutVerifying() 3 | ->get('https://example.com'); 4 | -------------------------------------------------------------------------------- /tests/fixtures/with-insecure-option.out: -------------------------------------------------------------------------------- 1 | Http::acceptJson() 2 | ->withoutVerifying() 3 | ->get('https://example.com'); 4 | -------------------------------------------------------------------------------- /tests/fixtures/missing-url-scheme.out: -------------------------------------------------------------------------------- 1 | Http::asForm() 2 | ->post('https://example.com', [ 3 | 'foo' => 'bar', 4 | ]); 5 | -------------------------------------------------------------------------------- /tests/fixtures/request-with-data.in: -------------------------------------------------------------------------------- 1 | curl -H 'Content-Type: application/x-www-form-urlencoded' -d 'foo=bar&baz=qui' https://example.com 2 | -------------------------------------------------------------------------------- /tests/fixtures/raw-data-mixed.in: -------------------------------------------------------------------------------- 1 | curl --request POST 'https://api.com' --header 'Accept: application/json' --data-raw 'some=data' -d foo=bar 2 | -------------------------------------------------------------------------------- /tests/fixtures/request-with-form-data.out: -------------------------------------------------------------------------------- 1 | Http::asMultipart() 2 | ->post('https://example.com', [ 3 | 'foo' => 'bar', 4 | ]); 5 | -------------------------------------------------------------------------------- /tests/fixtures/with-cert-path-only.out: -------------------------------------------------------------------------------- 1 | Http::withOptions([ 2 | 'cert' => '/path/to/cert', 3 | ]) 4 | ->get('https://example.com'); 5 | -------------------------------------------------------------------------------- /tests/fixtures/put-with-data.out: -------------------------------------------------------------------------------- 1 | Http::asForm() 2 | ->put('https://example.com', [ 3 | 'foo' => 'bar', 4 | 'baz' => 'qui', 5 | ]); 6 | -------------------------------------------------------------------------------- /tests/fixtures/request-with-data.out: -------------------------------------------------------------------------------- 1 | Http::asForm() 2 | ->post('https://example.com', [ 3 | 'foo' => 'bar', 4 | 'baz' => 'qui', 5 | ]); 6 | -------------------------------------------------------------------------------- /tests/fixtures/with-headers.out: -------------------------------------------------------------------------------- 1 | Http::withHeaders([ 2 | 'X-Header-One' => 1, 3 | 'X-Header-Two' => 2, 4 | ]) 5 | ->get('https://example.com'); 6 | -------------------------------------------------------------------------------- /tests/fixtures/raw-data-mixed.out: -------------------------------------------------------------------------------- 1 | Http::asForm() 2 | ->acceptJson() 3 | ->post('https://api.com', [ 4 | 'some' => 'data', 5 | 'foo' => 'bar', 6 | ]); 7 | -------------------------------------------------------------------------------- /tests/fixtures/stripe-query-params.in: -------------------------------------------------------------------------------- 1 | curl https://api.stripe.com/v1/customers/search -u sk_test_sabcdef0987654321: --data-urlencode query="name:'fakename' AND metadata['foo']:'bar'" -G 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-cert-and-key.out: -------------------------------------------------------------------------------- 1 | Http::withOptions([ 2 | 'cert' => '/path/to/cert', 3 | 'ssl_key' => '/path/to/key', 4 | ]) 5 | ->get('https://example.com'); 6 | -------------------------------------------------------------------------------- /tests/fixtures/stripe-example.in: -------------------------------------------------------------------------------- 1 | curl -X POST https://api.stripe.com/v1/charges -u sk_test_sjp0J5IpIZ4o7L0OtmCW3s7p: -d amount=2000 -d currency=usd -d source=tok_visa -d "metadata[order_id]"=6735 2 | -------------------------------------------------------------------------------- /tests/fixtures/with-query-string.in: -------------------------------------------------------------------------------- 1 | curl "https://api.postmarkapp.com/messages/outbound?recipient=john.doe@yahoo.com&count=50&offset=0&tag=welcome&status=sent&todate=2015-01-12&fromdate=2015-01-01" -X GET 2 | -------------------------------------------------------------------------------- /tests/fixtures/digital-ocean-example.out: -------------------------------------------------------------------------------- 1 | Http::withBody('{"name": "example.com", "ip_address": "127.0.0.1"}') 2 | ->withToken('abcdef0123456789') 3 | ->post('https://api.digitalocean.com/v2/domains'); 4 | -------------------------------------------------------------------------------- /tests/fixtures/with-raw-data.out: -------------------------------------------------------------------------------- 1 | Http::withBody('{ 2 | "messages": [ 3 | "a", 4 | "b", 5 | "c" 6 | ] 7 | }') 8 | ->acceptJson() 9 | ->post('https://api.com'); 10 | -------------------------------------------------------------------------------- /tests/fixtures/with-cert-path-and-password.out: -------------------------------------------------------------------------------- 1 | Http::withOptions([ 2 | 'cert' => [ 3 | '/path/to/cert', 4 | 'password', 5 | ], 6 | ]) 7 | ->get('https://example.com'); 8 | -------------------------------------------------------------------------------- /tests/fixtures/digital-ocean-example.in: -------------------------------------------------------------------------------- 1 | curl -H "Authorization: Bearer abcdef0123456789" -H "Content-Type: application/json" -d '{"name": "example.com", "ip_address": "127.0.0.1"}' -X POST "https://api.digitalocean.com/v2/domains" 2 | -------------------------------------------------------------------------------- /tests/fixtures/stripe-query-params.out: -------------------------------------------------------------------------------- 1 | Http::withBasicAuth('sk_test_sabcdef0987654321', '') 2 | ->get('https://api.stripe.com/v1/customers/search', [ 3 | 'query' => 'name:\'fakename\' AND metadata[\'foo\']:\'bar\'', 4 | ]); 5 | -------------------------------------------------------------------------------- /tests/fixtures/with-raw-data.in: -------------------------------------------------------------------------------- 1 | curl 'https://api.com' --header 'Accept: application/json' --header 'Content-Type: application/json' --data-raw '{ 2 | "messages": [ 3 | "a", 4 | "b", 5 | "c" 6 | ] 7 | }' 8 | -------------------------------------------------------------------------------- /tests/fixtures/post-with-data.out: -------------------------------------------------------------------------------- 1 | Http::asForm() 2 | ->post('https://example.com', [ 3 | 'foo' => 'bar', 4 | 'rho' => [ 5 | 1, 6 | 2, 7 | ], 8 | 'baz' => 'qui', 9 | ]); 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /tests/fixtures/post-with-form-data.out: -------------------------------------------------------------------------------- 1 | Http::asMultipart() 2 | ->post('https://example.com', [ 3 | 'foo' => 'bar', 4 | 'rho' => [ 5 | 1, 6 | 2, 7 | ], 8 | 'baz' => 'qui', 9 | ]); 10 | -------------------------------------------------------------------------------- /tests/fixtures/mailgun-example.in: -------------------------------------------------------------------------------- 1 | curl -s -X POST --user 'api:0987654321' https://api.mailgun.net/v3/example.com/messages -F from='Excited User ' -F to[]=YOU@YOUR_DOMAIN_NAME -F to[]=bar@example.com -F subject='Hello' -F text='Testing some Mailgun awesomeness!' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/build 3 | /public/hot 4 | /public/storage 5 | /storage/*.key 6 | /vendor 7 | .env 8 | .env.backup 9 | /.phpunit.cache 10 | Homestead.json 11 | Homestead.yaml 12 | auth.json 13 | npm-debug.log 14 | yarn-error.log 15 | /.idea 16 | /.vscode 17 | composer.lock 18 | docker-compose.yml 19 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | withBasicAuth('sk_test_sjp0J5IpIZ4o7L0OtmCW3s7p', '') 3 | ->post('https://api.stripe.com/v1/charges', [ 4 | 'amount' => 2000, 5 | 'currency' => 'usd', 6 | 'source' => 'tok_visa', 7 | 'metadata' => [ 8 | 'order_id' => 6735, 9 | ], 10 | ]); 11 | -------------------------------------------------------------------------------- /tests/fixtures/with-query-string.out: -------------------------------------------------------------------------------- 1 | Http::get('https://api.postmarkapp.com/messages/outbound', [ 2 | 'recipient' => 'john.doe@yahoo.com', 3 | 'count' => '50', 4 | 'offset' => '0', 5 | 'tag' => 'welcome', 6 | 'status' => 'sent', 7 | 'todate' => '2015-01-12', 8 | 'fromdate' => '2015-01-01', 9 | ]); 10 | -------------------------------------------------------------------------------- /src/Providers/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | commands([ 12 | CurlCommand::class, 13 | ]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/mailgun-example.out: -------------------------------------------------------------------------------- 1 | Http::asMultipart() 2 | ->withBasicAuth('api', '0987654321') 3 | ->post('https://api.mailgun.net/v3/example.com/messages', [ 4 | 'from' => 'Excited User ', 5 | 'to' => [ 6 | 'YOU@YOUR_DOMAIN_NAME', 7 | 'bar@example.com', 8 | ], 9 | 'subject' => 'Hello', 10 | 'text' => 'Testing some Mailgun awesomeness!', 11 | ]); 12 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "blank_line_before_statement": { 5 | "statements": ["return", "try"] 6 | }, 7 | "cast_spaces": { 8 | "space": "none" 9 | }, 10 | "concat_space": { 11 | "spacing": "one" 12 | }, 13 | "not_operator_with_successor_space": false, 14 | "logical_operators": true 15 | }, 16 | "notPath": [ 17 | "tests/fixtures" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/Feature 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ./app 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `curl` Converter 2 | Another bit of automation from [Shift](https://laravelshift.com) to convert `curl` requests to Laravel `Http` requests. See this project in action using Shift's [online curl converting](https://laravelshift.com/convert-curl-to-http). 3 | 4 | This project is a WIP. You may follow along with development in [Jason McCreary's live streams](https://www.youtube.com/watch?v=TpbkhR07W1g&list=PLmwAMIdrAmK5rH3SWvokHV8xI_0mauxDL&index=85). 5 | 6 | ## Contributing 7 | Code contributions may be made by submitting a [Pull Request](https://github.com/laravel-shift/curl-converter/compare) against the `main` branch. Submitted PRs should: 8 | 9 | - Explain the change, ideally using before and after examples. 10 | - Pass all build steps. 11 | - Ideally, include tests which verify the change. 12 | 13 | You may also contribute by [opening an issue](https://github.com/laravel-shift/curl-converter/issues/new) to report a bug or suggest a new feature. 14 | 15 | If you are so inclined, you may also say "thanks" or proclaim your love for [Shift on Twitter](https://twitter.com/laravelshift). 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Laravel Shift 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-shift/curl-converter", 3 | "description": "A command line tool to convert curl requests to Laravel HTTP requests.", 4 | "type": "library", 5 | "keywords": [ 6 | "curl", 7 | "converter", 8 | "http", 9 | "laravel", 10 | "shift" 11 | ], 12 | "license": "MIT", 13 | "require": { 14 | "php": "^8.3", 15 | "illuminate/console": "^11.0|^12.0", 16 | "illuminate/support": "^11.0|^12.0", 17 | "symfony/var-exporter": "^7.0" 18 | }, 19 | "require-dev": { 20 | "laravel/pint": "^1.24", 21 | "mockery/mockery": "^1.4.4", 22 | "orchestra/testbench": "^10.0", 23 | "phpunit/phpunit": "^11.5" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Shift\\CurlConverter\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Tests\\": "tests/" 33 | } 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "Shift\\CurlConverter\\Providers\\ServiceProvider" 39 | ] 40 | } 41 | }, 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true 47 | } 48 | -------------------------------------------------------------------------------- /src/Console/Commands/CurlCommand.php: -------------------------------------------------------------------------------- 1 | gatherOptions()); 17 | $code = HttpCall::format($request); 18 | 19 | $this->newLine(); 20 | $this->line($code); 21 | $this->newLine(); 22 | 23 | return 0; 24 | } 25 | 26 | private function gatherOptions() 27 | { 28 | return [ 29 | 'basic' => $this->option('basic'), 30 | 'cert' => $this->option('cert'), 31 | 'compressed' => $this->option('compressed'), 32 | 'connectTimeout' => $this->option('connect-timeout'), 33 | 'data' => $this->option('data'), 34 | 'dataUrlEncode' => $this->option('data-urlencode'), 35 | 'digest' => $this->option('digest'), 36 | 'fields' => $this->option('form'), 37 | 'headers' => $this->option('header'), 38 | 'insecure' => $this->option('insecure'), 39 | 'key' => $this->option('key'), 40 | 'maxTimeout' => $this->option('max-timeout'), 41 | 'method' => $this->option('get') ? 'GET' : $this->option('request'), 42 | 'rawData' => $this->option('data-raw'), 43 | 'retry' => $this->option('retry'), 44 | 'silent' => $this->option('curl-silent'), 45 | 'url' => $this->argument('url'), 46 | 'user' => $this->option('user'), 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Feature/Console/Commands/CurlCommandTest.php: -------------------------------------------------------------------------------- 1 | fixture($fixture . '.in')); 18 | $output = trim(Artisan::output()); 19 | 20 | $this->assertSame(0, $code); 21 | $this->assertSame($this->fixture($fixture . '.out'), $output); 22 | } 23 | 24 | public function test_it_throw_exception_when_for_invalid_url(): void 25 | { 26 | $this->expectException(InvalidArgumentException::class); 27 | $this->expectExceptionMessage('The "https://{domain:port}/api/{id}/" URL is invalid.'); 28 | 29 | Artisan::call('shift:curl -X GET "https://{domain:port}/api/{id}/"'); 30 | } 31 | 32 | public function test_it_throw_exception_when_for_invalid_headers(): void 33 | { 34 | $this->expectException(InvalidArgumentException::class); 35 | $this->expectExceptionMessage('The "foo" header must be a key/value pair separated by ":".'); 36 | 37 | Artisan::call("shift:curl https://example.com --header 'foo'"); 38 | } 39 | 40 | public static function curlCommandFixtures(): array 41 | { 42 | return [ 43 | 'GET request' => ['basic-get'], 44 | 'POST request' => ['basic-post'], 45 | 'POST request with data' => ['post-with-data'], 46 | 'POST request with JSON data' => ['post-json'], 47 | 'POST request with multipart/form-data' => ['post-with-form-data'], 48 | 'Request with data defaults to POST' => ['request-with-data'], 49 | 'Request with form fields defaults to POST' => ['request-with-form-data'], 50 | 'Request with collapsable headers' => ['with-collapsable-headers'], 51 | 'PUT request with data' => ['put-with-data'], 52 | 'GET request with headers' => ['with-headers'], 53 | 'GET request with query string' => ['with-query-string'], 54 | 'Mailgun example request' => ['mailgun-example'], 55 | 'Digital Ocean example request' => ['digital-ocean-example'], 56 | 'Stripe example request' => ['stripe-example'], 57 | 'Stripe query params' => ['stripe-query-params'], 58 | 'Initial connection timeout' => ['connect-timeout'], 59 | 'Entire transaction timeout' => ['max-timeout'], 60 | 'Ignore location flag' => ['ignore-location-flag'], 61 | 'Missing URL scheme' => ['missing-url-scheme'], 62 | 'GET request with compressed flag' => ['with-compressed-option'], 63 | 'GET request with insecure flag' => ['with-insecure-option'], 64 | 'GET request with short k flag' => ['with-insecure-k-option'], 65 | 'Request with raw data' => ['with-raw-data'], 66 | 'POST request with mixed data' => ['raw-data-mixed'], 67 | 'Request with cert (path only)' => ['with-cert-path-only'], 68 | 'Request with cert (path and password)' => ['with-cert-path-and-password'], 69 | 'Request with cert and key' => ['with-cert-and-key'], 70 | ]; 71 | } 72 | 73 | private function fixture(string $fixture) 74 | { 75 | return trim(file_get_contents('tests/fixtures/' . $fixture)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Support/HttpCall.php: -------------------------------------------------------------------------------- 1 | method()), 17 | self::generateRequest($request), 18 | ); 19 | } 20 | 21 | private static function collapseHelpers(array $headers, array &$options): array 22 | { 23 | return collect($headers) 24 | ->reject(function ($value, $header) use (&$options) { 25 | if ($header === 'Accept' && Str::lower($value) === 'application/json') { 26 | $options[] = 'acceptJson()'; 27 | 28 | return true; 29 | } 30 | 31 | if ($header === 'Authorization' && Str::of($value)->lower()->startsWith('bearer ')) { 32 | $options[] = 'withToken(\'' . substr($value, 7) . '\')'; 33 | 34 | return true; 35 | } 36 | 37 | return false; 38 | }) 39 | ->all(); 40 | } 41 | 42 | private static function filterHeaders(Request $request): array 43 | { 44 | return collect($request->headers()) 45 | ->reject(function ($value, $header) use ($request) { 46 | if ($request->data() && $header === 'Content-Type') { 47 | if ($request->isMultipartFormData() && Str::lower($value) === 'multipart/form-data') { 48 | return true; 49 | } elseif (Str::lower($value) === 'application/x-www-form-urlencoded') { 50 | return true; 51 | } 52 | } 53 | 54 | if ($header === 'Content-Type' && Str::lower($value) === 'application/json') { 55 | return true; 56 | } 57 | 58 | return false; 59 | }) 60 | ->all(); 61 | } 62 | 63 | private static function generateOptions(Request $request): string 64 | { 65 | $options = []; 66 | 67 | if (!empty($request->data()) && $request->method() !== 'GET') { 68 | if ($request->isMultipartFormData()) { 69 | $options[] = 'asMultipart()'; 70 | } elseif ($request->isRawData()) { 71 | $options[] = 'withBody(\'' . current($request->data()) . '\')'; 72 | } else { 73 | $options[] = 'asForm()'; 74 | } 75 | } 76 | 77 | $headers = self::filterHeaders($request); 78 | $headers = self::collapseHelpers($headers, $options); 79 | 80 | if ($request->hasUsernameOrPassword()) { 81 | $options[] = 'withBasicAuth(\'' . $request->username() . '\', \'' . $request->password() . '\')'; 82 | } 83 | 84 | if ($headers) { 85 | $options[] = 'withHeaders(' . self::prettyPrintArray($headers) . ')'; 86 | } 87 | 88 | if ($request->hasMaxTimeout()) { 89 | $options[] = 'timeout(' . $request->maxTimeout() . ')'; 90 | } 91 | 92 | if ($request->hasConnectTimeout()) { 93 | $options[] = 'connectTimeout(' . $request->connectTimeout() . ')'; 94 | } 95 | 96 | if (!empty($request->options())) { 97 | $options[] = 'withOptions(' . self::prettyPrintArray($request->options()) . ')'; 98 | } 99 | 100 | if ($request->isInsecure()) { 101 | $options[] = 'withoutVerifying()'; 102 | } 103 | 104 | if (empty($options)) { 105 | return ''; 106 | } 107 | 108 | return implode(PHP_EOL . ' ->', $options) . PHP_EOL . ' ->'; 109 | } 110 | 111 | private static function generateRequest(Request $request): string 112 | { 113 | if (empty($request->data()) || $request->isRawData()) { 114 | return "'" . $request->url() . "'"; 115 | } 116 | 117 | return sprintf('\'%s\', %s', $request->url(), self::prettyPrintArray($request->data())); 118 | } 119 | 120 | private static function prettyPrintArray(array $data) 121 | { 122 | return trim(preg_replace('/^/m', ' ', VarExporter::export($data))); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Models/Request.php: -------------------------------------------------------------------------------- 1 | url = $url; 37 | $this->method = Str::upper($method); 38 | } 39 | 40 | public static function create(array $data): self 41 | { 42 | $url = parse_url($data['url']); 43 | 44 | if ($url === false) { 45 | throw new InvalidArgumentException(sprintf('The "%s" URL is invalid.', $data['url'])); 46 | } 47 | 48 | $request = new self(self::buildUrl($url), $data['method'] ?? 'GET'); 49 | 50 | if (isset($url['query'])) { 51 | parse_str($url['query'], $request->data); 52 | } 53 | 54 | if (!empty($data['headers'])) { 55 | $request->headers = collect($data['headers']) 56 | ->mapWithKeys(function ($header) { 57 | if (!str_contains($header, ':')) { 58 | throw new InvalidArgumentException( 59 | sprintf('The "%s" header must be a key/value pair separated by ":".', $header) 60 | ); 61 | } 62 | 63 | [$key, $value] = explode(':', $header, 2); 64 | 65 | return [trim($key) => self::convertDataType(trim($value))]; 66 | }) 67 | ->all(); 68 | } 69 | 70 | if (!empty($data['dataUrlEncode'])) { 71 | $request->data = array_merge($request->data, self::parseData($data['dataUrlEncode'])); 72 | } 73 | 74 | if (!empty($data['rawData'])) { 75 | if (count($data['rawData']) === 1 && empty($data['data']) && empty($data['dataUrlEncode'])) { 76 | $request->data = $data['rawData']; 77 | $request->rawData = true; 78 | } else { 79 | $request->data = array_merge($request->data, self::parseData($data['rawData'])); 80 | } 81 | } 82 | 83 | if (!empty($data['data'])) { 84 | if (count($data['data']) === 1 && Str::startsWith($data['data'][0], '{')) { 85 | $request->data = $data['data']; 86 | $request->rawData = true; 87 | } else { 88 | $request->data = array_merge($request->data, self::parseData($data['data'])); 89 | } 90 | } 91 | 92 | if (!empty($data['fields'])) { 93 | $request->data = self::parseData($data['fields']); 94 | $request->multipartFormData = true; 95 | } 96 | 97 | if (is_null($data['method']) && (!empty($data['rawData']) || !empty($data['data']) || !empty($data['fields']))) { 98 | $request->method = 'POST'; 99 | } 100 | 101 | if ($data['user']) { 102 | [$request->username, $request->password] = explode(':', $data['user'], 2); 103 | } 104 | 105 | if ($data['maxTimeout']) { 106 | $request->maxTimeout = $data['maxTimeout']; 107 | } 108 | 109 | if ($data['connectTimeout']) { 110 | $request->connectTimeout = $data['connectTimeout']; 111 | } 112 | 113 | if (isset($data['cert'])) { 114 | @[$certificate, $password] = explode(':', $data['cert'], 2); 115 | 116 | if (isset($password)) { 117 | $request->guzzleOptions['cert'] = [$certificate, $password]; 118 | } else { 119 | $request->guzzleOptions['cert'] = $certificate; 120 | } 121 | } 122 | 123 | if (isset($data['key'])) { 124 | $request->guzzleOptions['ssl_key'] = $data['key']; 125 | } 126 | 127 | if ($data['insecure']) { 128 | $request->insecure = true; 129 | } 130 | 131 | return $request; 132 | } 133 | 134 | public function connectTimeout(): int 135 | { 136 | return $this->connectTimeout; 137 | } 138 | 139 | public function data(): array 140 | { 141 | return $this->data; 142 | } 143 | 144 | public function hasConnectTimeout(): bool 145 | { 146 | return isset($this->connectTimeout); 147 | } 148 | 149 | public function hasMaxTimeout(): bool 150 | { 151 | return isset($this->maxTimeout); 152 | } 153 | 154 | public function hasUsernameOrPassword(): bool 155 | { 156 | return isset($this->username) || isset($this->password); 157 | } 158 | 159 | public function headers(): array 160 | { 161 | return $this->headers; 162 | } 163 | 164 | public function isInsecure(): bool 165 | { 166 | return $this->insecure; 167 | } 168 | 169 | public function isMultipartFormData(): bool 170 | { 171 | return $this->multipartFormData; 172 | } 173 | 174 | public function isRawData(): bool 175 | { 176 | return $this->rawData; 177 | } 178 | 179 | public function maxTimeout(): int 180 | { 181 | return $this->maxTimeout; 182 | } 183 | 184 | public function method(): string 185 | { 186 | return $this->method; 187 | } 188 | 189 | public function options(): array 190 | { 191 | return $this->guzzleOptions; 192 | } 193 | 194 | public function password(): string 195 | { 196 | return $this->password ?? ''; 197 | } 198 | 199 | public function url(): string 200 | { 201 | return $this->url; 202 | } 203 | 204 | public function username(): string 205 | { 206 | return $this->username ?? ''; 207 | } 208 | 209 | private static function buildUrl(array $url): string 210 | { 211 | $output = ($url['scheme'] ?? 'https') . '://' . ($url['host'] ?? ''); 212 | 213 | if (isset($url['port'])) { 214 | $output .= ':' . $url['port']; 215 | } 216 | 217 | if (isset($url['path'])) { 218 | $output .= $url['path']; 219 | } 220 | 221 | return $output; 222 | } 223 | 224 | private static function convertDataType(string $value) 225 | { 226 | return preg_match('/^[1-9]\d*$/', $value) ? intval($value) : $value; 227 | } 228 | 229 | private static function parseData(array $data): array 230 | { 231 | parse_str(implode('&', $data), $data); 232 | array_walk_recursive($data, function (&$value) { 233 | if (is_string($value)) { 234 | $value = self::convertDataType($value); 235 | } 236 | }); 237 | 238 | return $data; 239 | } 240 | } 241 | --------------------------------------------------------------------------------