├── LICENSE ├── composer.json ├── config └── saloon.php ├── src ├── Casts │ ├── EncryptedOAuthAuthenticatorCast.php │ └── OAuthAuthenticatorCast.php ├── Console │ └── Commands │ │ ├── ListCommand.php │ │ ├── MakeAuthenticator.php │ │ ├── MakeCommand.php │ │ ├── MakeConnector.php │ │ ├── MakePlugin.php │ │ ├── MakeRequest.php │ │ └── MakeResponse.php ├── Events │ ├── SendingSaloonRequest.php │ └── SentSaloonRequest.php ├── Facades │ └── Saloon.php ├── Http │ ├── Faking │ │ └── MockClient.php │ └── Middleware │ │ ├── MockMiddleware.php │ │ ├── RecordResponse.php │ │ ├── SendRequestEvent.php │ │ └── SendResponseEvent.php ├── Saloon.php ├── SaloonServiceProvider.php └── Traits │ └── HasDeprecatedFacadeMethods.php └── stubs ├── saloon.authenticator.stub ├── saloon.connector.stub ├── saloon.oauth-connector.stub ├── saloon.plugin.stub ├── saloon.request.stub └── saloon.response.stub /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sam Carré 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": "saloonphp/laravel-plugin", 3 | "description": "The official Laravel plugin for Saloon", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "saloonphp", 8 | "saloon", 9 | "sdk", 10 | "api", 11 | "api-integrations" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Sam Carré", 16 | "email": "29132017+Sammyjo20@users.noreply.github.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "homepage": "https://github.com/saloonphp/laravel-plugin", 21 | "require": { 22 | "php": "^8.1", 23 | "illuminate/console": "^10.0 || ^11.0 || ^12.0", 24 | "illuminate/support": "^10.0 || ^11.0 || ^12.0", 25 | "saloonphp/saloon": "^3.5", 26 | "symfony/finder": "^6.4 || ^7.0" 27 | }, 28 | "require-dev": { 29 | "friendsofphp/php-cs-fixer": "^3.48", 30 | "orchestra/testbench": "^8.21 || ^9.0 || ^10.0", 31 | "pestphp/pest": "^2.32 || ^3.7", 32 | "phpstan/phpstan": "^1.10.56 || ^2.1" 33 | }, 34 | "minimum-stability": "stable", 35 | "prefer-stable": true, 36 | "autoload": { 37 | "psr-4": { 38 | "Saloon\\Laravel\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Saloon\\Laravel\\Tests\\": "tests/" 44 | } 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "pestphp/pest-plugin": true 49 | }, 50 | "sort-packages": true 51 | }, 52 | "extra": { 53 | "laravel": { 54 | "aliases": { 55 | "Saloon": "Saloon\\Laravel\\Facades\\Saloon" 56 | }, 57 | "providers": [ 58 | "Saloon\\Laravel\\SaloonServiceProvider" 59 | ] 60 | } 61 | }, 62 | "scripts": { 63 | "post-autoload-dump": [ 64 | "@php ./vendor/bin/testbench package:discover --ansi" 65 | ], 66 | "fix-code": [ 67 | "./vendor/bin/php-cs-fixer fix --allow-risky=yes" 68 | ], 69 | "pstan": [ 70 | "./vendor/bin/phpstan analyse" 71 | ], 72 | "test": [ 73 | "./vendor/bin/pest" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/saloon.php: -------------------------------------------------------------------------------- 1 | GuzzleSender::class, 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Integrations Path 26 | |-------------------------------------------------------------------------- 27 | | 28 | | By default, this package will create any classes within 29 | | `/app/Http/Integrations` directory. If you're using 30 | | a different design approach, then your classes 31 | | may be in a different place. You can change 32 | | that location so that the saloon:list 33 | | command will still find your files 34 | | 35 | */ 36 | 37 | 'integrations_path' => base_path('App/Http/Integrations'), 38 | ]; 39 | -------------------------------------------------------------------------------- /src/Casts/EncryptedOAuthAuthenticatorCast.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class EncryptedOAuthAuthenticatorCast implements CastsAttributes 15 | { 16 | /** 17 | * Cast the given value. 18 | */ 19 | public function get($model, string $key, $value, array $attributes): ?OAuthAuthenticator 20 | { 21 | if (is_null($value)) { 22 | return null; 23 | } 24 | 25 | return unserialize(decrypt($value), ['allowed_classes' => true]); 26 | } 27 | 28 | /** 29 | * Prepare the given value for storage. 30 | */ 31 | public function set($model, string $key, $value, array $attributes): ?string 32 | { 33 | if (is_null($value)) { 34 | return null; 35 | } 36 | 37 | if (! $value instanceof OAuthAuthenticator) { 38 | throw new InvalidArgumentException('The given value is not an OAuthAuthenticator instance.'); 39 | } 40 | 41 | return encrypt(serialize($value)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Casts/OAuthAuthenticatorCast.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class OAuthAuthenticatorCast implements CastsAttributes 15 | { 16 | /** 17 | * Cast the given value. 18 | */ 19 | public function get($model, string $key, $value, array $attributes): ?OAuthAuthenticator 20 | { 21 | if (is_null($value)) { 22 | return null; 23 | } 24 | 25 | return unserialize($value, ['allowed_classes' => true]); 26 | } 27 | 28 | /** 29 | * Prepare the given value for storage. 30 | */ 31 | public function set($model, string $key, $value, array $attributes): ?string 32 | { 33 | if (is_null($value)) { 34 | return null; 35 | } 36 | 37 | if (! $value instanceof OAuthAuthenticator) { 38 | throw new InvalidArgumentException('The given value is not an OAuthAuthenticator instance.'); 39 | } 40 | 41 | return serialize($value); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Console/Commands/ListCommand.php: -------------------------------------------------------------------------------- 1 | newLine(); 33 | 34 | $this->components->twoColumnDetail( 35 | 'General', 36 | 'Integrations: ' . count($this->getIntegrations()) . '' 37 | ); 38 | 39 | $this->newLine(); 40 | 41 | foreach ($this->getIntegrations() as $integration) { 42 | $this->getIntegrationOutput($integration); 43 | 44 | foreach ($this->getIntegrationAuthenticators($integration) as $integrationAuthenticator) { 45 | $this->getIntegrationAuthenticatorOutput($integrationAuthenticator); 46 | } 47 | 48 | foreach ($this->getIntegrationConnectors($integration) as $integrationConnector) { 49 | $this->getIntegrationConnectorOutput($integrationConnector); 50 | } 51 | 52 | foreach ($this->getIntegrationRequests($integration) as $integrationRequest) { 53 | $this->getIntegrationRequestOutput($integrationRequest); 54 | } 55 | 56 | foreach ($this->getIntegrationPlugins($integration) as $integrationPlugin) { 57 | $this->getIntegrationPluginOutput($integrationPlugin); 58 | } 59 | 60 | foreach ($this->getIntegrationResponses($integration) as $integrationResponse) { 61 | $this->getIntegrationResponseOutput($integrationResponse); 62 | } 63 | 64 | $this->newLine(); 65 | } 66 | 67 | return self::SUCCESS; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | protected function getIntegrations(): array 74 | { 75 | $finder = new Finder(); 76 | $integrations = []; 77 | 78 | foreach ($finder->directories()->in(config('saloon.integrations_path'))->depth(0) as $integration) { 79 | $integrations[] = $integration->getRealPath(); 80 | } 81 | 82 | return $integrations; 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | protected function getIntegrationConnectors(string $integration): array 89 | { 90 | $finder = new Finder(); 91 | $connectors = []; 92 | 93 | foreach ($finder->files()->in($integration)->depth(0) as $connector) { 94 | $connectors[] = $connector->getRealPath(); 95 | } 96 | 97 | return $connectors; 98 | } 99 | 100 | /** 101 | * @return array 102 | */ 103 | protected function getIntegrationRequests(string $integration): array 104 | { 105 | $finder = new Finder(); 106 | $requests = []; 107 | 108 | if (is_dir($integration . '/Requests')) { 109 | foreach ($finder->files()->in($integration . '/Requests') as $request) { 110 | $requests[] = $request->getRealPath(); 111 | } 112 | } 113 | 114 | return $requests; 115 | } 116 | 117 | /** 118 | * @return array 119 | */ 120 | protected function getIntegrationPlugins(string $integration): array 121 | { 122 | $finder = new Finder(); 123 | $plugins = []; 124 | 125 | if (is_dir($integration . '/Plugins')) { 126 | foreach ($finder->files()->in($integration . '/Plugins') as $plugin) { 127 | $plugins[] = $plugin->getRealPath(); 128 | } 129 | } 130 | 131 | return $plugins; 132 | } 133 | 134 | /** 135 | * @return array 136 | */ 137 | protected function getIntegrationResponses(string $integration): array 138 | { 139 | $finder = new Finder(); 140 | $responses = []; 141 | 142 | if (is_dir($integration . '/Responses')) { 143 | foreach ($finder->files()->in($integration . '/Responses') as $response) { 144 | $responses[] = $response->getRealPath(); 145 | } 146 | } 147 | 148 | return $responses; 149 | } 150 | 151 | /** 152 | * @return array 153 | */ 154 | protected function getIntegrationAuthenticators(string $integration): array 155 | { 156 | $finder = new Finder(); 157 | $authenticators = []; 158 | 159 | if (is_dir($integration . '/Auth')) { 160 | foreach ($finder->files()->in($integration . '/Auth') as $authenticator) { 161 | $authenticators[] = $authenticator->getRealPath(); 162 | } 163 | } 164 | 165 | return $authenticators; 166 | } 167 | 168 | protected function getIntegrationOutput(string $integration): void 169 | { 170 | $this->components->twoColumnDetail( 171 | '' . Str::afterLast($integration, '/') . '', 172 | sprintf( 173 | 'Authenticators: %s / Connectors: %s / Requests: %s / Plugins: %s / Responses: %s', 174 | count($this->getIntegrationAuthenticators($integration)), 175 | count($this->getIntegrationConnectors($integration)), 176 | count($this->getIntegrationRequests($integration)), 177 | count($this->getIntegrationPlugins($integration)), 178 | count($this->getIntegrationResponses($integration)), 179 | ) 180 | ); 181 | } 182 | 183 | protected function getIntegrationAuthenticatorOutput(string $authenticator): void 184 | { 185 | $this->components->twoColumnDetail( 186 | 'Authenticator ... ' . $authenticator 187 | ); 188 | } 189 | 190 | protected function getIntegrationConnectorOutput(string $connector): void 191 | { 192 | $this->components->twoColumnDetail( 193 | 'Connector ....... ' . $connector, 194 | '' . $this->getIntegrationConnectorBaseUrl($connector) . '' 195 | ); 196 | } 197 | 198 | protected function getIntegrationRequestOutput(string $request): void 199 | { 200 | $requestMethod = Str::afterLast($this->getIntegrationRequestMethod($request), ':'); 201 | 202 | $requestMethodOutputColour = match ($requestMethod) { 203 | 'GET' => 'blue', 204 | 'PATCH', 'POST', 'PUT' => 'green', 205 | 'DELETE' => 'red', 206 | default => 'magenta' 207 | }; 208 | 209 | $this->components->twoColumnDetail( 210 | 'Request ......... ' . 211 | $request, 212 | ' ' . $this->getIntegrationRequestEndpoint($request) . '' . 213 | ' ' . 214 | Str::afterLast($this->getIntegrationRequestMethod($request), ':') . ' ' 215 | ); 216 | } 217 | 218 | 219 | protected function getIntegrationPluginOutput(string $plugin): void 220 | { 221 | $this->components->twoColumnDetail( 222 | 'Plugin .......... ' . $plugin 223 | ); 224 | } 225 | 226 | 227 | protected function getIntegrationResponseOutput(string $response): void 228 | { 229 | $this->components->twoColumnDetail( 230 | 'Response ........ ' . $response 231 | ); 232 | } 233 | 234 | protected function getIntegrationRequestMethod(string $request): string 235 | { 236 | $contents = file_get_contents($request); 237 | 238 | if ($contents === false) { 239 | $contents = ''; 240 | } 241 | 242 | return Str::match('/\$method\s*=\s*(.*?);/', $contents); 243 | } 244 | 245 | 246 | protected function getIntegrationRequestEndpoint(string $request): string 247 | { 248 | $contents = file_get_contents($request); 249 | 250 | if ($contents === false) { 251 | $contents = ''; 252 | } 253 | 254 | $regex = '/public\s+function\s+resolveEndpoint\(\):\s+string\s*\{\s*return\s+(.*?);/s'; 255 | 256 | $match = Str::match($regex, $contents); 257 | $matchSegments = explode('/', $match); 258 | 259 | foreach ($matchSegments as $key => $matchSegment) { 260 | if (Str::contains($matchSegment, '$this->')) { 261 | $matchSegments[$key] = '{' . Str::before( 262 | Str::after(str_replace(' ', '', $matchSegment), '>'), 263 | '.\'' 264 | ) . '}'; 265 | } 266 | } 267 | 268 | return str_replace('\'', '', implode('/', $matchSegments)); 269 | } 270 | 271 | 272 | protected function getIntegrationConnectorBaseUrl(string $connector): string 273 | { 274 | $contents = file_get_contents($connector); 275 | 276 | if ($contents === false) { 277 | $contents = ''; 278 | } 279 | 280 | $regex = '/public\s+function\s+resolveBaseUrl\(\):\s+string\s*\{\s*return\s+\'(.*?)\';\s*/s'; 281 | $matches = Str::match($regex, $contents); 282 | 283 | return Str::after($matches, '://'); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeAuthenticator.php: -------------------------------------------------------------------------------- 1 | resolveStubPath('/../../../stubs/' . $this->resolveStubName()); 30 | } 31 | 32 | protected function resolveStubName(): string 33 | { 34 | return method_exists($this, 'stub') 35 | ? $this->stub() 36 | : $this->stub; 37 | } 38 | 39 | /** 40 | * Resolve the fully-qualified path to the stub. 41 | * 42 | * @param string $stub 43 | * @return string 44 | */ 45 | protected function resolveStubPath($stub) 46 | { 47 | return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) 48 | ? $customPath 49 | : __DIR__ . $stub; 50 | } 51 | 52 | /** 53 | * Get the default namespace for the class. 54 | * 55 | * @param string $rootNamespace 56 | * @return string 57 | */ 58 | protected function getDefaultNamespace($rootNamespace) 59 | { 60 | $namespace = $this->getNamespaceFromIntegrationsPath() . $this->getFormattedNamespace(); 61 | 62 | return str_replace('{integration}', $this->getIntegration(), $rootNamespace . $namespace); 63 | } 64 | 65 | protected function getIntegration(): string 66 | { 67 | $integration = $this->argument('integration'); 68 | 69 | if (! is_string($integration)) { 70 | throw new \LogicException('The {integration} argument must be a string.'); 71 | } 72 | 73 | return $integration; 74 | } 75 | 76 | /** 77 | * Get the console command arguments. 78 | * 79 | * @return array 80 | */ 81 | protected function getArguments(): array 82 | { 83 | return [ 84 | ['integration', InputArgument::REQUIRED, 'The related integration'], 85 | ...parent::getArguments(), 86 | ]; 87 | } 88 | 89 | /** 90 | * Returns the namespace without the default Saloon parts 91 | */ 92 | protected function getFormattedNamespace(): string 93 | { 94 | return str_replace('\\Http\\Integrations', '', $this->namespace); 95 | } 96 | 97 | /** 98 | * Converts the integrations path to a namespace friendly string 99 | */ 100 | protected function getNamespaceFromIntegrationsPath(): string 101 | { 102 | $namespace = (array)str_replace(['\\App', '\\app'], '', str_replace('/', '\\', str_replace(base_path(), '', config('saloon.integrations_path')))); 103 | 104 | return $namespace[0]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeConnector.php: -------------------------------------------------------------------------------- 1 | option('oauth') 44 | ? 'saloon.oauth-connector.stub' 45 | : 'saloon.connector.stub'; 46 | } 47 | 48 | /** 49 | * Get the options for making a connector 50 | * 51 | * @return array> 52 | */ 53 | protected function getOptions(): array 54 | { 55 | return [ 56 | ['oauth', null, InputOption::VALUE_NONE, 'Whether the connector should include the OAuth boilerplate'], 57 | ]; 58 | } 59 | 60 | /** 61 | * Interact further with the user if they were prompted for missing arguments. 62 | * 63 | * @return void 64 | */ 65 | protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) 66 | { 67 | if ($this->didReceiveOptions($input)) { 68 | return; 69 | } 70 | 71 | $supportOauth = $this->confirm('Should the connector support OAuth? (Authorization Code Grant)'); 72 | 73 | $input->setOption('oauth', $supportOauth); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Console/Commands/MakePlugin.php: -------------------------------------------------------------------------------- 1 | > 57 | */ 58 | protected function getOptions(): array 59 | { 60 | return [ 61 | ['method', 'm', InputOption::VALUE_REQUIRED, 'the method of the request'], 62 | ]; 63 | } 64 | 65 | /** 66 | * Interact further with the user if they were prompted for missing arguments. 67 | */ 68 | protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void 69 | { 70 | if ($this->didReceiveOptions($input)) { 71 | return; 72 | } 73 | 74 | $methodType = select( 75 | 'What method should the saloon request send?', 76 | Arr::pluck(Method::cases(), 'name') 77 | ); 78 | 79 | $input->setOption('method', $methodType); 80 | } 81 | 82 | /** 83 | * Build the class with the given name. 84 | * 85 | * @param string $name 86 | * 87 | * @throws FileNotFoundException 88 | */ 89 | protected function buildClass($name): MakeRequest|string 90 | { 91 | $method = $this->option('method') ?? 'GET'; 92 | 93 | if (! is_string($method)) { 94 | throw new InvalidArgumentException('The method option must be a string.'); 95 | } 96 | 97 | $stub = $this->files->get($this->getStub()); 98 | $stub = $this->replaceMethod($stub, $method); 99 | 100 | return $this->replaceNamespace($stub, $name)->replaceClass($stub, $name); 101 | } 102 | 103 | /** 104 | * Replace the method for the stub 105 | */ 106 | protected function replaceMethod(string $stub, string $name): string 107 | { 108 | return str_replace('{{ method }}', $name, $stub); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeResponse.php: -------------------------------------------------------------------------------- 1 | pendingRequest = $pendingRequest; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/SentSaloonRequest.php: -------------------------------------------------------------------------------- 1 | pendingRequest = $pendingRequest; 31 | $this->response = $response; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Facades/Saloon.php: -------------------------------------------------------------------------------- 1 | $responses) Alias of MockClient::global() 16 | * @method static MockClient mockClient() Alias of MockClient::global() 17 | * @method static void assertSent(string|callable $value) 18 | * @method static void assertNotSent(string|callable $value) 19 | * @method static void assertNothingSent() 20 | * @method static void assertSentCount(int $count) 21 | */ 22 | class Saloon extends Facade 23 | { 24 | /** 25 | * Register the Saloon facade 26 | */ 27 | protected static function getFacadeAccessor(): string 28 | { 29 | return 'saloon'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http/Faking/MockClient.php: -------------------------------------------------------------------------------- 1 | $responses 23 | * @return $this 24 | */ 25 | public function startMocking(array $responses = []): self 26 | { 27 | $this->addResponses($responses); 28 | 29 | $this->isMocking = true; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Check if we are mocking 36 | */ 37 | public function isMocking(): bool 38 | { 39 | return $this->isMocking; 40 | } 41 | 42 | /** 43 | * Resolve the MockClient from the container 44 | */ 45 | public static function resolve(): static 46 | { 47 | return resolve(static::class); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/Middleware/MockMiddleware.php: -------------------------------------------------------------------------------- 1 | isMocking() === false) { 24 | return; 25 | } 26 | 27 | if ($pendingRequest->hasMockClient() || $pendingRequest->hasFakeResponse()) { 28 | return; 29 | } 30 | 31 | if ($mockClient->isEmpty()) { 32 | throw new NoMockResponseFoundException($pendingRequest); 33 | } 34 | 35 | $pendingRequest->withMockClient($mockClient); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Middleware/RecordResponse.php: -------------------------------------------------------------------------------- 1 | getPendingRequest(), $response); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Saloon.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected array $recordedResponses = []; 31 | 32 | /** 33 | * Start mocking! 34 | * 35 | * @param array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable> $responses 36 | */ 37 | public static function fake(array $responses): MockClient 38 | { 39 | $mockClient = static::mockClient(); 40 | 41 | $mockClient->addResponses($responses); 42 | 43 | return $mockClient; 44 | } 45 | 46 | /** 47 | * Retrieve the global mock client 48 | */ 49 | public static function mockClient(): MockClient 50 | { 51 | return MockClient::getGlobal() ?? MockClient::global(); 52 | } 53 | 54 | /** 55 | * Assert that a given request was sent. 56 | */ 57 | public static function assertSent(string|callable $value): void 58 | { 59 | static::mockClient()->assertSent($value); 60 | } 61 | 62 | /** 63 | * Assert that a given request was not sent. 64 | */ 65 | public static function assertNotSent(string|callable $value): void 66 | { 67 | static::mockClient()->assertNotSent($value); 68 | } 69 | 70 | /** 71 | * Assert JSON data was sent 72 | * 73 | * @param array $data 74 | */ 75 | public static function assertSentJson(string $request, array $data): void 76 | { 77 | static::mockClient()->assertSentJson($request, $data); 78 | } 79 | 80 | /** 81 | * Assert that nothing was sent. 82 | */ 83 | public static function assertNothingSent(): void 84 | { 85 | static::mockClient()->assertNothingSent(); 86 | } 87 | 88 | /** 89 | * Assert a request count has been met. 90 | */ 91 | public static function assertSentCount(int $count): void 92 | { 93 | static::mockClient()->assertSentCount($count); 94 | } 95 | 96 | /** 97 | * Start Saloon recording responses. 98 | * 99 | * @deprecated This method will be removed in Saloon v4. 100 | */ 101 | public function record(): void 102 | { 103 | $this->record = true; 104 | } 105 | 106 | /** 107 | * Stop Saloon recording responses. 108 | * 109 | * @deprecated This method will be removed in Saloon v4. 110 | */ 111 | public function stopRecording(): void 112 | { 113 | $this->record = false; 114 | } 115 | 116 | /** 117 | * Check if Saloon is recording 118 | * 119 | * @deprecated This method will be removed in Saloon v4. 120 | */ 121 | public function isRecording(): bool 122 | { 123 | return $this->record; 124 | } 125 | 126 | /** 127 | * Record a response. 128 | * 129 | * @deprecated This method will be removed in Saloon v4. 130 | */ 131 | public function recordResponse(Response $response): void 132 | { 133 | $this->recordedResponses[] = $response; 134 | } 135 | 136 | /** 137 | * Get all the recorded responses. 138 | * 139 | * @deprecated This method will be removed in Saloon v4. 140 | * 141 | * @return array<\Saloon\Http\Response> 142 | */ 143 | public function getRecordedResponses(): array 144 | { 145 | return $this->recordedResponses; 146 | } 147 | 148 | /** 149 | * Get the last response that Saloon recorded. 150 | * 151 | * @deprecated This method will be removed in Saloon v4. 152 | */ 153 | public function getLastRecordedResponse(): ?Response 154 | { 155 | if (empty($this->recordedResponses)) { 156 | return null; 157 | } 158 | 159 | $lastResponse = end($this->recordedResponses); 160 | 161 | reset($this->recordedResponses); 162 | 163 | return $lastResponse; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/SaloonServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 32 | __DIR__ . '/../config/saloon.php', 33 | 'saloon' 34 | ); 35 | } 36 | 37 | /** 38 | * Handle the booting of the service provider. 39 | */ 40 | public function boot(): void 41 | { 42 | $this->app->bind('saloon', Saloon::class); 43 | $this->app->singleton(MockClient::class, fn () => new MockClient); 44 | 45 | if ($this->app->runningInConsole()) { 46 | $this->registerCommands(); 47 | } 48 | 49 | $this->publishes([ 50 | __DIR__ . '/../config/saloon.php' => config_path('saloon.php'), 51 | ], 'saloon-config'); 52 | 53 | // Register Saloon Laravel's Global Middleware 54 | 55 | if (! Saloon::$registeredDefaults) { 56 | Config::setSenderResolver(static function (): Sender { 57 | /** @var Sender */ 58 | return new (config('saloon.default_sender')); 59 | }); 60 | 61 | Config::globalMiddleware() 62 | ->onRequest(new MockMiddleware, 'laravelMock') 63 | ->onRequest(new SendRequestEvent, 'laravelSendRequestEvent', PipeOrder::LAST) 64 | ->onResponse(new RecordResponse, 'laravelRecordResponse', PipeOrder::FIRST) 65 | ->onResponse(new SendResponseEvent, 'laravelSendResponseEvent', PipeOrder::FIRST); 66 | 67 | Saloon::$registeredDefaults = true; 68 | } 69 | 70 | // Destroy global mock client to prevent leaky tests 71 | 72 | BaseMockClient::destroyGlobal(); 73 | } 74 | 75 | /** 76 | * Register the Saloon commands 77 | * 78 | * @return $this 79 | */ 80 | protected function registerCommands(): self 81 | { 82 | $this->commands([ 83 | MakeConnector::class, 84 | MakeRequest::class, 85 | MakeResponse::class, 86 | MakePlugin::class, 87 | MakeAuthenticator::class, 88 | ListCommand::class, 89 | ]); 90 | 91 | return $this; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Traits/HasDeprecatedFacadeMethods.php: -------------------------------------------------------------------------------- 1 | $data 18 | * 19 | * @deprecated This method will be removed in Saloon v4. 20 | */ 21 | public static function assertSentJson(string $request, array $data): void 22 | { 23 | // This trait is only used to present deprecations. 24 | } 25 | 26 | /** 27 | * Start Saloon recording responses. 28 | * 29 | * @deprecated This method will be removed in Saloon v4. 30 | */ 31 | public function record(): void 32 | { 33 | // This trait is only used to present deprecations. 34 | } 35 | 36 | /** 37 | * Stop Saloon recording responses. 38 | * 39 | * @deprecated This method will be removed in Saloon v4. 40 | */ 41 | public function stopRecording(): void 42 | { 43 | // This trait is only used to present deprecations. 44 | } 45 | 46 | /** 47 | * Check if Saloon is recording 48 | * 49 | * @deprecated This method will be removed in Saloon v4. 50 | */ 51 | public function isRecording(): bool 52 | { 53 | // This trait is only used to present deprecations. 54 | } 55 | 56 | /** 57 | * Record a response. 58 | * 59 | * @deprecated This method will be removed in Saloon v4. 60 | */ 61 | public function recordResponse(Response $response): void 62 | { 63 | // This trait is only used to present deprecations. 64 | } 65 | 66 | /** 67 | * Get all the recorded responses. 68 | * 69 | * @deprecated This method will be removed in Saloon v4. 70 | * 71 | * @return array<\Saloon\Http\Response> 72 | */ 73 | public function getRecordedResponses(): array 74 | { 75 | // This trait is only used to present deprecations. 76 | } 77 | 78 | /** 79 | * Get the last response that Saloon recorded. 80 | * 81 | * @deprecated This method will be removed in Saloon v4. 82 | */ 83 | public function getLastRecordedResponse(): ?Response 84 | { 85 | // This trait is only used to present deprecations. 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /stubs/saloon.authenticator.stub: -------------------------------------------------------------------------------- 1 | setClientId('') 30 | ->setClientSecret('') 31 | ->setRedirectUri('') 32 | ->setDefaultScopes([]) 33 | ->setAuthorizeEndpoint('authorize') 34 | ->setTokenEndpoint('token') 35 | ->setUserEndpoint('user'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stubs/saloon.plugin.stub: -------------------------------------------------------------------------------- 1 |